Summary
When an OpenClaw run is interrupted after persisting an assistant message with toolCall blocks but before all matching toolResult messages are written, reopening the session can leave unresolved tail tool calls in transcript history.
In the same family of failures, persisted orphan user turns on reopen should be preserved explicitly rather than silently rewound.
Why this matters
This is a runtime/session-transcript integrity issue, not a plugin issue.
The affected seams are around:
- session transcript persistence
- reopen/startup transcript reconciliation
- toolCall/toolResult pairing at the tail of history
- interrupted-run recovery semantics
User-visible impact:
- dangling tail
toolCalls remain in transcript history
- reopen behavior depends on local repair behavior instead of explicit first-party source truth
- interrupted sessions can produce confusing repaired history unless compensation is deterministic and append-only
Repro shape
A minimal broken tail looks like one of these:
- assistant tool call with no trailing tool result
{ "role": "assistant", "content": [{ "type": "toolCall", "id": "tc_1", "name": "read" }], "stopReason": "tool_use" }
- assistant tool calls with only partial trailing tool results
{ "role": "assistant", "content": [
{ "type": "toolCall", "id": "tc_1", "name": "exec" },
{ "type": "toolCall", "id": "tc_2", "name": "read" }
]}
{ "role": "toolResult", "toolCallId": "tc_1", "toolName": "exec", ... }
- persisted orphan user tail
{ "role": "user", "content": [{ "type": "text", "text": "please continue" }] }
followed by process interruption before any next assistant turn is persisted.
Expected behavior
On reopen/startup, the runtime should:
- preserve existing transcript rows
- append explicit synthetic recovery rows only at the tail
- append synthetic error
toolResult rows for unresolved tail tool calls
- append one explicit assistant recovery marker for a persisted orphan-user tail
- be idempotent on repeated reopen
- never re-execute tools during repair
- never silently rewind or delete prior transcript rows
Safe compensation strategy
The bounded first slice that worked well locally is:
- repair unresolved tail-only tool calls before prompt build
- then run orphan-user preservation logic
- use append-only synthetic recovery rows
- log structured repair telemetry
- do nothing for already-healthy transcripts
Validation performed locally
I validated this behavior in a local installed-package environment with targeted executable regressions covering:
- orphan-user replay marker append
- unresolved tail toolCall -> synthetic error toolResult repair
- idempotent second reopen
- healthy transcript no-op
- seam/order check that tail repair runs before orphan-user repair
Important environment note
The installed package available locally did not include a full upstream editable source tree / test harness suitable for a normal upstream patch cycle, so the local validation was done as:
- runtime-active bundle patch for immediate behavior validation
- installed-package source-truth overlay + targeted executable tests for parity evidence
That means this issue is not claiming a clean upstream repo patch was already merged. The request here is to absorb this behavior into official first-party runtime source + tests.
Requested upstream fix
Please add a first-party source-level transcript compensation seam that:
- repairs unresolved tail
toolCall / toolResult pairing on reopen
- preserves orphan-user tails with an explicit assistant recovery marker
- guarantees append-only, idempotent repair
- includes upstream tests for:
- missing tail toolResult
- orphan-user replay
- repeated reopen no-op
- healthy transcript no-op
Non-goals for this issue
This is not asking for:
- deep historical transcript rewrite
- tool re-execution
- fabricated successful outputs
- a full write-atomicity redesign in the same patch
Those can be separate follow-ups if needed.
Summary
When an OpenClaw run is interrupted after persisting an assistant message with
toolCallblocks but before all matchingtoolResultmessages are written, reopening the session can leave unresolved tail tool calls in transcript history.In the same family of failures, persisted orphan user turns on reopen should be preserved explicitly rather than silently rewound.
Why this matters
This is a runtime/session-transcript integrity issue, not a plugin issue.
The affected seams are around:
User-visible impact:
toolCalls remain in transcript historyRepro shape
A minimal broken tail looks like one of these:
{ "role": "assistant", "content": [{ "type": "toolCall", "id": "tc_1", "name": "read" }], "stopReason": "tool_use" }{ "role": "assistant", "content": [ { "type": "toolCall", "id": "tc_1", "name": "exec" }, { "type": "toolCall", "id": "tc_2", "name": "read" } ]} { "role": "toolResult", "toolCallId": "tc_1", "toolName": "exec", ... }{ "role": "user", "content": [{ "type": "text", "text": "please continue" }] }followed by process interruption before any next assistant turn is persisted.
Expected behavior
On reopen/startup, the runtime should:
toolResultrows for unresolved tail tool callsSafe compensation strategy
The bounded first slice that worked well locally is:
Validation performed locally
I validated this behavior in a local installed-package environment with targeted executable regressions covering:
Important environment note
The installed package available locally did not include a full upstream editable source tree / test harness suitable for a normal upstream patch cycle, so the local validation was done as:
That means this issue is not claiming a clean upstream repo patch was already merged. The request here is to absorb this behavior into official first-party runtime source + tests.
Requested upstream fix
Please add a first-party source-level transcript compensation seam that:
toolCall/toolResultpairing on reopenNon-goals for this issue
This is not asking for:
Those can be separate follow-ups if needed.