Skip to content

fix(tau3-bench-banking): end episodes whose conversation outgrows any context window - #151

Closed
abhinav-pola wants to merge 1 commit into
mainfrom
devin/1792000059-tau3-conversation-cap
Closed

abhinav-pola wants to merge 1 commit into
mainfrom
devin/1792000059-tau3-conversation-cap

Conversation

@abhinav-pola

@abhinav-pola abhinav-pola commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

tau3 banking now ends an episode with CONVERSATION_TOO_LARGE (scored as a failure) once the conversation passes 16M characters, instead of the trial pod running out of JavaScript heap.

What changed?

  • solver.ts: tracks the conversation's total characters (content, reasoning, tool call names and arguments) as messages are appended. Above MAX_CONVERSATION_CHARS (16,000,000) it stops running the remaining tool calls in the turn and ends the episode with the new TerminationReason.ConversationTooLarge.
  • solver.test.ts: covers messageChars and checks that a runaway parallel-tool-call turn goes over the cap.

Why?

In the mimo-v2.6-flash price-order run, 19 trials crashed with JavaScript heap out of memory (exit 134, about 507MB heap in a 1Gi pod). In each crashed task, ClickHouse shows a Makora generation with 500–7,221 parallel tool calls in one turn, ending in tool_calls. The solver runs every call, and each KB_search returns its top 10 full documents (about 1.5KB each, so roughly 15KB per call). A 4,000-call turn adds about 60MB of tool-result text, and building the next request copies it several more times.

16M characters is about 4M tokens, well above any context window in use. The largest prompt that succeeded in these runs was about 909k tokens. A request this large would be rejected upstream, so the cap only replaces a pod crash with a failed episode.

How to test

bun test src/benchmarks/tau3-bench-banking

End to end: after the subtree sync, rerun the crashed mimo-v2.6-flash price-order tasks. Any that hit the runaway turn should finish as CONVERSATION_TOO_LARGE instead of exiting with code 134.

Benchmark impact

Scores are unchanged for every episode under 16M characters. Episodes above the cap used to crash the trial and now score 0, since the evaluator fails any termination other than user or agent stop.

Reviewer focus

  • The root cause is inferred from ClickHouse tool-call counts and document sizes. No heap snapshot confirms it, because the local reruns didn't reproduce the runaway turn.

Checklist

  • Tests cover changed behavior
  • Public API or configuration changes are backward compatible, or the break is documented
  • Benchmark changes document dataset provenance and licensing
  • No credentials, private results, or restricted dataset contents are included
  • Documentation is updated where needed

Link to Devin session: https://openrouter.devinenterprise.com/sessions/72a3f13c847645c5b06306d88cec089d
Open in Devin Desktop: https://openrouter.devinenterprise.com/desktop/session/72a3f13c847645c5b06306d88cec089d?variant=devin
Requested by: @abhinav-pola


Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from Abhinav

SYSTEM:
<latest_message>
Abhinav Pola (U090K0G7JF3) [ts=1791572889.759909]: @Devin move the tool call fuzz dataset to the public repo and sync it back to the subtree
</latest_message>

=== BEGIN THREAD HISTORY (in #agents-benchmarks) ===
Abhinav Pola (U090K0G7JF3) [ts=1791572889.759909]: @Devin move the tool call fuzz dataset to the public repo and sync it back to the subtree
=== END THREAD HISTORY ===
Channel ID: C0BAKP8P5C3
Thread URL: https://openrouter.slack.com/archives/C0BAKP8P5C3/p1791572889759909?thread_ts=1791572889.759909&amp;cid=C0BAKP8P5C3

The <latest_message> is the message that you should use to guide your goals + task for this session, and you should use the rest of the slack thread as context.
A [ts=...] marker on a Slack message is that message's timestamp. To act on a specific message with the slack tool (e.g. adding an emoji reaction via the reaction command), pass that value as timestamp along with the Channel ID — no extra lookup call is needed.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +124 to +126
return (
message.content.length + (message.reasoning?.length ?? 0) + toolCallChars
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Encrypted reasoning bypasses the conversation cap

When the model returns large encrypted reasoning, messageChars counts none of that payload. messagesToResponses replays responseItems verbatim, so oversized requests can still exhaust the trial's heap.

Learn more

The assistant output from responsesTurnToModelOutput includes the provider's raw responseItems. The provider requests encrypted reasoning and extractReasoning does not put encrypted content in message.reasoning. The next request uses messagesToResponses, which forwards the entire responseItems array instead of reconstructing it from counted fields. The counter therefore stays low even when those raw items make the next request large.

Example: An assistant turn contains responseItems: [{type: "reasoning", encrypted_content: "x".repeat(17_000_000)}] with empty content and no tool calls. The counter adds zero, then the solver sends the large raw item again instead of ending the episode.

Recommended fix: Count the serialized payload actually replayed from responseItems when present, including encrypted reasoning, while avoiding double-counting the synthesized fields for that same request. Add a solver-level regression that returns an oversized raw reasoning item and verifies termination before another model.generate.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +269 to +272
if (conversationChars > MAX_CONVERSATION_CHARS) {
terminationReason = TerminationReason.ConversationTooLarge;
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Oversized final turns evade cap termination

When an oversized user turn includes a stop token, pushMessage leaves UserStop in place, allowing a passing score. At the step or tool-error limit, earlier guards likewise replace ConversationTooLarge with another reason.

Learn more

The solver appends each message through pushMessage, but this check only runs at the start of a subsequent iteration. A user response containing a stop token sets done and never reaches that iteration; a turn reaching the step or error limit reaches the preceding guards first. The evaluator accepts UserStop but fails an oversized-conversation reason.

Example: A user response longer than 16,000,000 characters ends with ###STOP###. The solver records USER_STOP and can score it normally, while an equally oversized non-stop response records CONVERSATION_TOO_LARGE unless it hits another earlier limit.

Recommended fix: Resolve the cap immediately after each append and before accepting a stop or other termination, or prioritize cap termination above other guards and explicitly check the final user turn. Cover both stop-token and limit-boundary cases in a solver-level test.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant