Skip to content

fix(sessions): close out unanswered tool calls before the error reply (tool-batch wedge) - #1796

Merged
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/tool-batch-history-wedge
Aug 7, 2026
Merged

fix(sessions): close out unanswered tool calls before the error reply (tool-batch wedge)#1796
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/tool-batch-history-wedge

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Red-first TDD. This PR currently contains only a failing test that
reproduces the bug — CI is expected to be red. The fix lands in a follow-up
commit; the test flips green once it does.

The bug

This is the root cause of the self-hosted/DeepSeek session wedges from the
tool-call investigation. When a parallel tool batch partially fails,
LlmSessionActor.FailCurrentTurn appends the "I encountered an error executing
a tool"
assistant reply (via SessionState.AddErrorReply) without first
writing a tool-result for the unanswered call(s)
. History becomes:

assistant  tool_calls=[A, B]
tool       result A
assistant  "I encountered an error executing a tool"   ← wedged here
tool       result B   (synthetic, appended on a later turn)

Strict OpenAI-compatible providers (DeepSeek, Qwen, vLLM) require an assistant
tool_calls message to be immediately followed by a contiguous run of
tool-result messages answering every tool_call_id. The wedged error reply
breaks that run, so every subsequent request returns HTTP 400
insufficient tool messages following tool_calls and the session is stuck until
a new user turn rebuilds history.

The failing test

ToolBatchHistoryWedgeTests drives a two-call parallel batch where one call
throws in InterpretToolCall — the only pre-try seam in
SessionToolExecutionPipeline that escapes to ToolExecutionFailed, so the
healthy call is recorded first (a Task.WhenAll invariant). It then asserts, on
the history assembled for the next provider request, that the assistant
tool_calls message is immediately followed by tool-results for every call
id.

Today it fails with:

Expected [call-A,call-B] but the contiguous run covered [call-A].
Assembled roles: system -> system -> user -> assistant -> tool -> assistant -> tool -> user

— the assistant(tool_calls) -> tool(A) -> assistant(error) -> tool(B) wedge,
exactly as the provider would see it.

The fix (follow-up commit)

Make FailCurrentTurn close out the unanswered tail tool batch (synthesize
tool-results for the still-open call ids, reusing the existing
ParkedToolBatchHistory / ToolBatchAbandoned machinery) before appending
the error reply — so history is well-formed at rest for any provider, and no
mid-batch tool failure can wedge a session.

…y wedge

Reproduces the bug behind the self-hosted/DeepSeek session wedge. When a
parallel tool batch partially fails, LlmSessionActor.FailCurrentTurn appends the
"I encountered an error executing a tool" assistant reply without first writing
a tool-result for the unanswered call. History becomes
[assistant tool_calls(A,B), tool A, assistant error, tool B] — the error reply
is wedged between the tool_calls message and the rest of its results. That
breaks the contiguity strict OpenAI-compatible providers (DeepSeek, Qwen, vLLM)
require, so every later turn fails with HTTP 400
"insufficient tool messages following tool_calls" and the session stays stuck.

This is the failing (red) half of a red-green change. It drives a two-call
parallel batch where one call throws in InterpretToolCall — the only pre-try
seam that reaches ToolExecutionFailed, so the healthy call is recorded first —
and asserts, on the history assembled for the next request, that the assistant
tool_calls message is immediately followed by a contiguous run of tool-result
messages answering every call id.

The fix follows in a separate commit.
Fixes the tool-batch history wedge. When a parallel tool batch partially fails,
the ToolExecutionFailed handler now closes out the still-unanswered call(s) with
synthetic tool-results — reusing the existing ParkedToolBatchHistory /
ToolBatchAbandoned machinery — BEFORE FailCurrentTurn appends the
"I encountered an error executing a tool" assistant reply.

History stays [assistant tool_calls(A,B), tool A, tool B, assistant error] — a
contiguous tool-result run — instead of wedging the error reply between the
results. Strict OpenAI-compatible providers (DeepSeek, Qwen, vLLM) reject the
wedged shape with HTTP 400 "insufficient tool messages following tool_calls" on
every later turn, which stuck the session.

Flips ToolBatchHistoryWedgeTests from red to green; the full Sessions suite
stays green.
@Aaronontheweb Aaronontheweb added bug Something isn't working reliability Retries, resilience, graceful degradation sessions LLM session actor, turn lifecycle, pipelines labels Aug 7, 2026
@Aaronontheweb
Aaronontheweb marked this pull request as ready for review August 7, 2026 17:33

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

LGTM

@Aaronontheweb
Aaronontheweb merged commit dead7d8 into netclaw-dev:dev Aug 7, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/tool-batch-history-wedge branch August 7, 2026 17:34
@Aaronontheweb Aaronontheweb mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working reliability Retries, resilience, graceful degradation sessions LLM session actor, turn lifecycle, pipelines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant