```app-1 | openhands.sdk.conversation.except...
# questions
a
Copy code
app-1       | openhands.sdk.conversation.exceptions.ConversationRunError: Conversation run failed for id=c02b8a60-1825-42df-90ab-72745c0cadfb: litellm.BadRequestError: AnthropicException - {"type":"error","error":{"type":"invalid_request_error","message":"messages.2: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01Cnyhua59bGtcZjuVDt4gYm. Each `tool_use` block must have a corresponding `tool_result` block in the next message."},"request_id":"req_011CW7D3TS6djwSvoFGrwN3h"}
app-1       |
Been getting this a lot when there is a lot of events and right after a condensation. Gonna try to investigate EDIT: Bug found, repro test in the thread
It doesn't trigger this edge case somehow if i lower the condensation threshold
okay i think it might have something to do with the fact that the summary event is inserted in weird places
Copy code
Bug: Condensation summary can be inserted between action-observation pairs, breaking LLM API message ordering
Summary
The LLMSummarizingCondenser uses a fixed summary_offset (equal to keep_first) when creating a Condensation event. However, after _enforce_batch_atomicity and filter_unmatched_tool_calls modify the kept_events list, the offset may no longer point to a safe insertion position. This can result in the CondensationSummaryEvent being inserted between an ActionEvent and its corresponding ObservationEvent, breaking the required message ordering for LLM APIs.
Root Cause
In LLMSummarizingCondenser.get_condensation():
return Condensation(    forgotten_event_ids=[event.id for event in forgotten_events],    summary=summary,    summary_offset=self.keep_first,  # Fixed offset based on original view    ...)
Later, in View.from_events():
_enforce_batch_atomicity() may forget additional events (expanding forgotten_event_ids)
filter_unmatched_tool_calls() removes orphaned actions/observations
The summary is inserted at summary_offset before the final filtering
After these transformations, the event list structure changes, but summary_offset remains fixed at keep_first, potentially landing between an action and its observation.
Reproduction
When condensation occurs and:
Some events from the original "head" are filtered out (e.g., due to batch atomicity)
The remaining events don't align with the original keep_first boundary
Example observed in production:
[0] SystemPromptEvent[1] MessageEvent[2] ObservationEvent [orphaned - action was forgotten][3] ActionEvent [tool_call_id=abc123][4] ObservationEvent [tool_call_id=abc123] --> INSERT HERE (summary_offset=4)
The summary is inserted between [3] and [4], which share the same tool_call_id.
Expected Behavior
The summary should be inserted at a position that does not break:
Action-observation pairs (matched by tool_call_id)
Consecutive actions from the same LLM response batch (matched by llm_response_id)
Actual Behavior
The summary can be inserted between an ActionEvent and its ObservationEvent, resulting in malformed LLM API requests:
assistant message (with tool_use)user message (summary)  ← breaks the sequencetool message (result)
This violates API requirements (especially Anthropic) where tool results must immediately follow the assistant's tool_use block.
test_view_summary_offset.py
Copy code
@staticmethod
    def _find_safe_summary_offset(
        events: list[LLMConvertibleEvent],
        desired_offset: int,
    ) -> int:
        """Find a safe offset for inserting the summary that doesn't break
        action-observation pairs.

        The summary should not be inserted between:
        1. Consecutive ActionEvents from the same LLM response batch
        2. An ActionEvent and its corresponding ObservationEvent

        This method adjusts the offset forward to find a safe insertion point.
        """
        if desired_offset >= len(events):
            return len(events)

        if desired_offset <= 0:
            return 0

        # Build a map of tool_call_id -> action index for matching
        action_indices: dict[ToolCallID, int] = {}
        for i, event in enumerate(events):
            if isinstance(event, ActionEvent):
                action_indices[event.tool_call_id] = i

        # Check if the offset lands in an unsafe position and adjust forward
        offset = desired_offset
        while offset < len(events):
            is_safe = True

            # Check 1: Don't insert between consecutive actions from same batch
            if offset > 0 and offset < len(events):
                prev_event = events[offset - 1]
                curr_event = events[offset]

                # If previous is an action, check if current is also an action
                # from the same LLM response
                if isinstance(prev_event, ActionEvent) and isinstance(
                    curr_event, ActionEvent
                ):
                    if prev_event.llm_response_id == curr_event.llm_response_id:
                        is_safe = False
                        logger.debug(
                            f"Offset {offset} unsafe: between actions of same batch"
                        )

            # Check 2: Don't insert between an action and its observation
            if is_safe and offset > 0:
                prev_event = events[offset - 1]

                # If previous event is an action, check if any observation
                # for this action exists later in the list
                if isinstance(prev_event, ActionEvent):
                    action_tool_call_id = prev_event.tool_call_id
                    # Look for matching observation after the offset
                    for j in range(offset, len(events)):
                        event = events[j]
                        if (
                            isinstance(event, ObservationBaseEvent)
                            and event.tool_call_id == action_tool_call_id
                        ):
                            # Found observation for this action after offset
                            # This means we'd be inserting between action and obs
                            is_safe = False
                            logger.debug(
                                f"Offset {offset} unsafe: between action and "
                                f"observation (tool_call_id={action_tool_call_id})"
                            )
                            break

            if is_safe:
                return offset

            offset += 1

        return len(events)

....
        if summary is not None and summary_offset is not None:
            # logger.debug(f"Inserting summary at offset {summary_offset}")
            # Clamp summary_offset to valid range
            summary_offset = min(summary_offset, len(kept_events))

            # Adjust offset to avoid inserting between an action and its observation.
            # Find a safe insertion point by ensuring we don't split action-observation pairs.
            adjusted_offset = View._find_safe_summary_offset(kept_events, summary_offset)

            logger.debug(
                f"Inserting summary at offset {adjusted_offset} "
                f"(original: {summary_offset})"
            )

            _new_summary_event = CondensationSummaryEvent(summary=summary)
            # kept_events.insert(summary_offset, _new_summary_event)
            kept_events.insert(adjusted_offset, _new_summary_event)
...
This might work
@calm-artist-7206 want to lyk of this