Skip to content

email-agent: cross-turn conversation history is the confirmed primary cause of #2763's context overflow #2781

Description

@itomek

Problem

Investigating #2763 (email agent context-overflow bug) found the confirmed primary cause of that failure is upstream of anything search_messages itself controls: whatever code path assembles a later chat turn's prompt from earlier turns' results is carrying substantially more than a compact summary of prior turns, and nothing prunes it.

Measured live (GPU, Gemma-4-E4B-it-GGUF, ctx 65536, real Gmail, real Lemonade error) — a realistic 2-turn TUI session:

  1. Turn 1, "What's new in my inbox today?" → the agent calls pre_scan_inbox (which itself makes triage_inbox + detect_waiting_on_you sub-calls) → answers correctly.
  2. Turn 2, "how many emails from Every in the last two weeks?" → the agent calls search_messages once → the request overflows: Lemonade's own error reports n_prompt_tokens: 72438 against n_ctx: 65536 (over by 6,902 / 10.5%).

Turn 2's own search_messages call, measured against a representative fixture with the real Gemma tokenizer, costs on the order of ~13.8K tokens. Fixed per-turn overhead (system prompt + the 65-tool OpenAI schema), also real-tokenizer-measured, is ~20.8K tokens. That accounts for ~34.6K of the 72,438 total — the other ~37.8K tokens came from somewhere else, and the only candidates are the auto-scan pre_scan_inbox call the agent/TUI makes on session start and turn 1's own explicit pre_scan_inbox call, both still resident in the prompt when turn 2 executes.

If those two pre_scan_inbox-family results are comparable in size (both scanned the same ~1,408-message inbox at the same 25-message default), each is contributing on the order of ~19K tokens — labeled here as an estimate from arithmetic (measured total minus the two real-tokenizer-measured components), not a per-call measurement; a live re-run with per-call size logging would firm this up (see Acceptance Criteria).

A related observability gap surfaced while trying to get exact per-call numbers: log_tool_call's (verbose.py) structured extra dict — which carries tool_args and result_summary, exactly the fields needed to diagnose this class of problem — is not rendered by the plain-text log formatter in the sidecar's on-disk log. Only tool_call name=X / tool_result name=X ok=... latency=... reach the file; the arguments and result size do not. Anyone trying to diagnose a context-budget problem after the fact hits this same wall. Worth a cheap fix (render extra as a compact suffix, or add a JSON-lines sink) alongside or before the measurement work above.

This is a general finding, not an email-agent-specific one: whatever assembles "context" from prior turns is not doing what the driving-the-tui skill's own documentation says it should ("the transcript pushed back as context must carry a compact record of what was displayed") — either that compaction isn't happening for tool-result content, or it's happening for the card display but not for what actually reaches the model, or it doesn't exist yet. This issue is to find out which and fix it; #2763's own fix (making the new turn's own tool result cheap) is a mitigation that leaves more headroom, not a fix for this accumulation.

Outcome

A multi-turn conversation's prompt size is understood and bounded: either prior turns genuinely contribute only a compact record (matching the documented intent), or there is an explicit, measured budget for how much prior-turn content is allowed to carry forward before something prunes it — never an unbounded carry-forward that silently consumes most of the context window before the current turn's own content is considered.

Acceptance criteria

  • Identify the actual code path that assembles a later turn's messages/prompt from earlier turns (TUI context field, the /v1/email/query request body, or an SDK-level history mechanism — name the real one, don't assume). Expected artifact: a comment on this issue naming the file/function and quoting what it currently does with a prior turn's tool result (verbatim summary vs. full content).
  • Measure, per prior turn, what it actually contributes to the next turn's prompt (chars/tokens) with a live capture — confirm or correct this issue's ~19K-per-pre_scan_inbox-result estimate with a real number, the way fix(email-agent): search_messages returns no answer at all for a long-bodied sender — context overflow #2763's own investigation got a real n_prompt_tokens from Lemonade's error response.
  • Decide and implement a bound: either genuinely compact prior-turn tool results (matching the driving-the-tui skill's documented intent) or cap how many/how much prior-turn content rides along, with a fail-loud message if a turn is dropped from context because the cap was hit — never a silent, unbounded carry-forward.
  • Regression test: a 3+ turn session where turns 1 and 2 each produce a large tool result; assert turn 3's prompt size stays under a stated budget regardless of how large turns 1-2's raw results were.

Scope & expectations

How to verify

Live capture on GPU + Gmail, same shape as #2763's investigation: a multi-turn session, with the real n_prompt_tokens (or an equivalent per-turn size log) recorded at each turn, showing the fix keeps a later turn's prompt within budget regardless of how much earlier turns produced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent::emailEmail agent changesbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions