[Bug]: t3_thread_read results overflow Claude Code's MCP output limit on long threads#33417

opened2d agobyAAviddaram

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Use a thread with a few thousand timeline items. Mine has 3,589.
  2. From Claude Code, call t3_thread_read on it with no options, which means view: messages, limit: 50, maxCharsPerItem: 20000.
  3. Call it again with { view: "activity", limit: 100, maxCharsPerItem: 1500 }. That's the call agents make most.
  4. Call it again with { view: "activity", limit: 100, maxCharsPerItem: 1 }.

Response sizes from a 0.1.8-nightly.20261104.4192 server:

callchars
defaults (messages, 50 × 20000)69,714
defaults, view: activity121,806
activity, 100 × 1500136,430
activity, 100 × 160,271

Claude Code rejects MCP results over MAX_MCP_OUTPUT_TOKENS, which defaults to 25k tokens. For this JSON that works out to about 50k chars. In my transcripts the largest t3_thread_read result that got through was 48,608 chars and the smallest one rejected was 54,205.

Expected behavior

A read with default or documented options fits in the agent's tool output limit. An agent that wants the latest messages gets them in one call.

Actual behavior

There are two causes.

  1. Pages always start at the oldest item. ProjectionStore.getTimelinePage takes matching.slice(0, options.limit) after afterPosition. With no cursor, a read returns the beginning of the thread. To reach the newest message an agent has to walk forward with afterPosition=nextPosition, which is 36 pages of 100 on this thread. T3's own CLI works around this. readLatestMessages widens a backward window, doubling it each pass, to find the last few messages.
  2. Nothing bounds the total size. limit × maxCharsPerItem only bounds item text, and the defaults allow 50 × 20,000. Every item also carries 500 to 650 chars of metadata. itemId alone is about 143 chars and contains the thread id twice, and sourceThreadId and runId repeat it again. So limit: 100, maxCharsPerItem: 1 still returns 60k chars. Lowering maxCharsPerItem can't make a 100-item page fit.

What agents actually hit, from 538 Claude Code sessions on my machine:

  • 308 t3_thread_read calls. 137 of them (44%) were rejected for size, in 16 sessions.
  • 133 of those 137 already passed limit: 100 and a small maxCharsPerItem (usually 1500), as the tool description asks ("Keep reads small").
  • One session read a single thread 48 times, walking afterPosition 99, 199, 299 … 4099 to reach the end. 41 of those reads were rejected.

When a result is rejected, Claude Code saves it to a file. The agent then reads that file back in slices or writes a script to filter it. That costs extra turns and context, and T3 never sees any of it.

Impact

Major degradation or frequent failure

Version or commit

0.1.8-nightly.20261104.4192. The same code is on main @ b8c2cfad6b.

Environment

Linux 6.17, Claude Code 2.1.319 (default MAX_MCP_OUTPUT_TOKENS), Node 26.10. Threads run on Codex and Claude providers.

Logs or stack traces

shell
1Error: result (136,430 characters) exceeds maximum allowed tokens. Output has been saved to <path>

Workaround

Agents call t3_thread_read with limit ≤ 20, or walk pages inside a script and keep only the tail. Neither helps an agent that doesn't know the thread is long.


I'd like to send the PR for this, and I have a fix in mind:

  1. Response budget in readThread. Stop adding items once the encoded result reaches about 30k chars, and report hasMore as today. Always return at least one item, cut with the existing textTruncated / nextTextOffset. A read then fits whatever options the agent passes.
  2. Newest page by default. With no cursor, return the newest limit items, still in chronological order inside the page. Add beforePosition to go back, and return earlierPosition with the result. afterPosition keeps working as now for "what's new since my last read". The CLI's readLatestMessages becomes a single call.
  3. Lower defaults (limit 20, maxCharsPerItem 4000, runLimit 3), and a tool description that says how to page in each direction.

I simulated this against the same live threads. The newest 20 messages come to 16k to 20k chars. The newest 20 activity items come to 14k to 28k with the budget, against 46k without it on one thread.

Point 2 changes a default, so I'd like a yes or no on it before I build it. If you'd rather keep the oldest-first default, I'll send 1 and 3 alone.