OpenHands / OpenHands/software-agent-sdk

Incremental view path skips enforce_properties after condensation, leaving orphaned tool_results/tool_uses for the LLM

Open Beginner friendly
#4,498 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug condenser-test priority:medium release-note-required
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Bug

The incremental view path (ConversationState.view hot path) does not call enforce_properties after applying a Condensation, so a condensation that forgets an action but keeps its observation (or vice versa) leaves an orphaned tool_result (or tool_use) in the view that gets sent to the LLM. Anthropic (and other providers) reject this with a malformed-conversation-history error such as:

tool_use ids were found without tool_result blocks immediately after

or

each tool_result block must have a corresponding tool_use block in the previous message

Root cause

View.append_event applies a Condensation by calling event.apply(self.events), which removes forgotten events by ID. This can break action/observation pairs — for example, forgetting an ActionEvent (tool_use) while keeping its ObservationEvent (tool_result), or the other way around.

The ToolCallMatchingProperty and ObservationUniquenessProperty exist precisely to drop such orphans, but enforce_properties only runs on the rebuild path (View.from_eventsenforce_properties). The incremental path (ConversationState.view fast path, which calls append_event for each new tail event) intentionally skips enforce_properties for performance (see #3053 and test_hot_path_does_not_call_enforce_properties).

As a result, when a condensation is applied incrementally and breaks a pair, the orphan stays in the cached view and is sent to the LLM on the next step.

Repro

from openhands.sdk.context.view import View
from openhands.sdk.event.condenser import Condensation
from openhands.sdk.event.llm_convertible import ActionEvent, MessageEvent, ObservationEvent
from openhands.sdk.llm import Message, MessageToolCall, TextContent
from openhands.sdk.mcp.definition import MCPToolAction, MCPToolObservation

msg = MessageEvent(llm_message=Message(role="user", content=[TextContent(text="hi")]), source="user")
a1 = ActionEvent(thought=[], action=MCPToolAction(data={}), tool_name="t", tool_call_id="c1",
                 tool_call=MessageToolCall(id="c1", name="t", arguments="{}", origin="completion"),
                 llm_response_id="r1", source="agent")
o1 = ObservationEvent(observation=MCPToolObservation.from_text(text="ok", tool_name="t"),
                      tool_name="t", tool_call_id="c1", action_id="a1", source="environment")

view = View()
for e in [msg, a1, o1]:
    view.append_event(e)

# Condensation forgets the action but not the observation.
view.append_event(Condensation(forgotten_event_ids={a1.id}, llm_response_id="r2"))

# Before the fix: view still contains o1 (orphan tool_result).
# After the fix: o1 is dropped by enforce_properties.
ids = {e.id for e in view.events}
assert o1.id not in ids  # orphan must be dropped

Before the fix, o1.id is still in the view; View.from_events (rebuild path) would have dropped it.

Fix

Call self.enforce_properties(self.events) at the end of the Condensation branch in View.append_event. Using self.events as the all_events reference is safe: the pair-matching properties (ToolCallMatchingProperty, ObservationUniquenessProperty) only inspect the current view events, and the all_events-dependent properties (BatchAtomicityProperty, ToolLoopAtomicityProperty) become no-ops when all_events == current view events (they detect no missing batches/loops because the reference set is the same). The full enforcement still runs on rebuild_view() (cold load, fork, error recovery).

This does not affect the hot-path performance for normal (non-condensation) appends — enforce_properties is only called after a Condensation is applied.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in openhands/sdk/context/view.py at View.append_event and its Condensation branch. Reproduce the orphan case from the issue, then run the relevant view tests, including test_hot_path_does_not_call_enforce_properties; done means incremental condensation drops unmatched tool events without changing normal append behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.