The repair was synthesising placeholder tool results for any dangling
call, including calls in the last assistant message that are simply
pending (interrupted for approval/question). This broke durable resume
because the provider saw the placeholder and thought the tool already
ran, so resolve_inbox could not re-execute the tool.
Now trailing calls (assistant tool_calls as the last message with no
result) are left untouched — the engine will resume them. Placeholders
are only injected when the thread has moved past the call, proving it
is corrupt rather than pending.
Fixes test_durable_resume_question and
test_durable_resume_approval_executes_tool.
When a turn is interrupted at the wrong moment, the append-only JSONL can
end up with a user message between an assistant tool_calls block and its
tool result. Providers reject this ordering (Anthropic 400/2013, OpenAI
'tool_call_ids did not have response messages'), making the session
permanently unrecoverable from the UI.
_repair_tool_pairing() runs in ConversationStore.load() and:
- moves a real tool result found later in the thread to sit right after its call
- synthesises a placeholder result for a call with no matching tool message
- is idempotent — well-formed threads pass through unchanged
Fixes#331