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:
- 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.
- 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
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.
Problem
Investigating #2763 (email agent context-overflow bug) found the confirmed primary cause of that failure is upstream of anything
search_messagesitself 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:pre_scan_inbox(which itself makestriage_inbox+detect_waiting_on_yousub-calls) → answers correctly.search_messagesonce → the request overflows: Lemonade's own error reportsn_prompt_tokens: 72438againstn_ctx: 65536(over by 6,902 / 10.5%).Turn 2's own
search_messagescall, 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-scanpre_scan_inboxcall the agent/TUI makes on session start and turn 1's own explicitpre_scan_inboxcall, 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) structuredextradict — which carriestool_argsandresult_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. Onlytool_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 (renderextraas 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-tuiskill'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
messages/prompt from earlier turns (TUIcontextfield, the/v1/email/queryrequest 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).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 realn_prompt_tokensfrom Lemonade's error response.driving-the-tuiskill'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.Scope & expectations
src/gaia/ui/(TUI/API request layer) or wherever the sidecar's/v1/email/queryendpoint builds a turn'smessagesfrom session history — notread_tools.py/context_budget.py(fix(email-agent): search_messages returns no answer at all for a long-bodied sender — context overflow #2763's own scope) oragent.py's single-turn overflow handling (filed separately, fix(agent): context-overflow shrink-and-retry recovers 81 tokens of a 6,902-token overflow #2780).search_messagesfix (a mitigation already shipped, not a substitute for this). fix(agent): context-overflow shrink-and-retry recovers 81 tokens of a 6,902-token overflow #2780 (the shrink-and-retry recovering almost nothing) — related but distinct: that's about salvaging one overflowing turn; this is about why so much was already resident before that turn started.driving-the-tuiskill's "card/context trap" section is the existing design intent to align with, not override.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.