Incremental view path skips enforce_properties after condensation, leaving orphaned tool_results/tool_uses for the LLM
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 90/100
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.
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_useids were found withouttool_resultblocks immediately after
or
each
tool_resultblock must have a correspondingtool_useblock 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_events → enforce_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.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 542
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 139
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OpenHands/software-agent-sdk
-
acp bug duplicate-candidate priority:medium ready-for-dev
Difficulty 1/5 Under an hour Newbie friendliness 93/100
OpenHands/software-agent-sdk#5171 · 2 comments ·
-
bug llm priority:medium ready-for-dev sdk
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
OpenHands/software-agent-sdk#5168 · 12 comments · 1 reaction ·
-
Document the invariant: every LLM request must send a system message before the first user message Opendocumentation
Difficulty 2/5 1-2 days Newbie friendliness 76/100
OpenHands/software-agent-sdk#5150 · 1 comment ·
-
invariants
Difficulty 1/5 Under an hour Newbie friendliness 85/100
OpenHands/software-agent-sdk#5146 · 1 comment ·
-
invariants
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
OpenHands/software-agent-sdk#5145 · 1 comment ·
All issues in OpenHands/software-agent-sdk
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100