Skip to content

fix: recognize snake_case tool call types in session history repair#50802

Open
lishuaigit wants to merge 4 commits intoopenclaw:mainfrom
lishuaigit:fix/session-repair-snake-case-tool-types
Open

fix: recognize snake_case tool call types in session history repair#50802
lishuaigit wants to merge 4 commits intoopenclaw:mainfrom
lishuaigit:fix/session-repair-snake-case-tool-types

Conversation

@lishuaigit
Copy link
Copy Markdown
Contributor

Summary

  • Problem: After an interrupted Anthropic tool call, users get persistent "Session history looks corrupted (tool call input missing)" errors that /new cannot reliably resolve.
  • Root cause: isRawToolCallBlock() in session-transcript-repair.ts only recognizes camelCase block types (toolCall, toolUse, functionCall) but Anthropic persists tool_use (snake_case). The broken block survives repairToolCallInputs() sanitization, and Anthropic rejects the request with "tool_use.input: field required".
  • Fix: Add tool_call, tool_use, and function_call to the type check so the repair logic can detect and fix (or drop) broken snake_case blocks.

Change Type

  • Bug fix

Scope

  • Session history / transcript repair

Linked Issue

User-visible / Behavior Changes

Before: Interrupted Anthropic tool call → persistent "session history corrupted" error → /new doesn't fix it

After: Broken tool_use blocks are detected and dropped by the sanitizer → session recovers automatically

Security Impact

None.

Evidence

  • 25 tests passing (23 existing + 2 new snake_case tests)
✓ recognizes snake_case tool call block types (tool_use, tool_call, function_call)
✓ drops snake_case tool_use blocks missing input

Compatibility

  • Backward compatible. Only adds more type strings to an existing check.

Risks

None — the added types follow the exact same repair logic as the existing camelCase types.

[AI-assisted development by OpenClaw agent 虾干 🦐]

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 20, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/openclaw/openclaw/blob/f72c134ea75a747529c16c4dc5fb575260e8ab8f/src/agents/session-transcript-repair.ts#L262-L266
P1 Badge Re-add snake_case tool blocks when rebuilding assistant content

Now that isRawToolCallBlock() returns true for tool_use/tool_call, this nested check still only handles the camelCase variants. When the same assistant turn also contains any dropped or rewritten block, repairToolCallInputs() emits nextContent without the valid snake_case block, so sanitizeToolCallInputs() can drop the assistant message entirely. I reproduced this with a turn containing a valid tool_use plus one malformed toolCall; the sanitizer returned only the following toolResult, which later repair logic treats as orphaned.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 20, 2026

Greptile Summary

This PR extends isRawToolCallBlock() to recognize Anthropic's native snake_case block types (tool_use, tool_call, function_call), allowing the session history repair logic to detect and drop malformed blocks that were previously invisible to the sanitizer. The core intent is sound and addresses the reported persistent "session history corrupted" error.

However, the inner type guard inside repairToolCallInputs (lines 262–266) was not updated alongside isRawToolCallBlock, creating a concrete gap:

  • sessions_spawn attachment redaction is bypassed for snake_case blocks. sanitizeToolCallBlock — which redacts sensitive inline attachment content — is only reached when the camelCase branch (toolCall/toolUse/functionCall) is matched. A sessions_spawn call arriving with type tool_use will survive all drop checks but never be redacted, potentially persisting raw attachment content (marked __OPENCLAW_REDACTED__ for camelCase calls) to session storage.
  • Snake_case blocks are silently lost in mixed-type messages. If messageChanged is set true by a sibling camelCase block in the same assistant message (e.g. a trimmed name or a redacted sessions_spawn), the loop uses nextContent at the end — which was never populated for the snake_case block — silently dropping it without incrementing droppedToolCalls.

The new tests pass because they use pure snake_case-only messages, which leave messageChanged = false and fall through to out.push(msg), preserving the original unchanged. This masks both failure modes above.

Key issues:

  • Inner type check at src/agents/session-transcript-repair.ts:262-266 must be extended with the same six types present in isRawToolCallBlock to route snake_case blocks through the sanitization path.
  • A test covering sessions_spawn with type tool_use and attachments should be added to confirm redaction is applied.
  • A test covering an assistant message with both a camelCase and a snake_case tool call should be added to confirm the snake_case block is not silently dropped.

Confidence Score: 2/5

  • Not safe to merge as-is: the incomplete inner type guard leaves a security gap (sessions_spawn attachments unredacted for snake_case blocks) and a silent data-loss path in mixed-type messages.
  • The drop-path fix in isRawToolCallBlock works correctly for the narrow case of pure snake_case messages with missing input. But the sanitization-path guard (lines 262–266) was not updated, so (1) sessions_spawn redaction is never applied to tool_use/tool_call/function_call blocks and (2) snake_case blocks are silently omitted from nextContent whenever any sibling block has already set messageChanged = true. Both gaps are undetected by the new tests, which only cover the pure-snake_case no-mutation path.
  • src/agents/session-transcript-repair.ts — specifically the inner type guard at lines 262–266 inside repairToolCallInputs.

Comments Outside Diff (1)

  1. src/agents/session-transcript-repair.ts, line 261-297 (link)

    P1 Snake_case blocks bypass sanitization and are silently lost in mixed-type messages

    The inner type guard (lines 262–266) was not updated alongside isRawToolCallBlock, so it still only matches the three camelCase variants. This creates two concrete problems:

    1. sessions_spawn attachment redaction is skipped for tool_use blocks.
    sanitizeToolCallBlock — which strips sensitive inline attachment content — is only reached when the inner if (camelCase) branch is entered. A sessions_spawn call that arrives with Anthropic's native tool_use type will pass isRawToolCallBlock, survive the drop check (it has input), but then fall through the inner guard without ever calling sanitizeToolCallBlock. The raw attachment content is persisted to session history un-redacted.

    2. Valid snake_case blocks are silently dropped when any sibling camelCase block has been mutated.
    If messageChanged becomes true for another block in the same content array (e.g. a camelCase toolUse whose sessions_spawn attachments were redacted, or a padded name that was trimmed), the loop exits and the code reaches out.push({ ...msg, content: nextContent }) — using nextContent, which never received the snake_case block. The block disappears from the session without being counted as dropped.

    The reason the two new tests pass despite this gap: in a message containing only snake_case blocks no mutation is triggered, so messageChanged stays false and droppedInMessage stays 0, falling through to out.push(msg) which preserves the untouched original msg. This is accidentally correct for that narrow case but masks the two failure modes above.

    The fix is to add the snake_case types to the inner guard so they receive the same sanitization path:

    // src/agents/session-transcript-repair.ts – lines 262-266
          if (
            (block as { type?: unknown }).type === "toolCall" ||
            (block as { type?: unknown }).type === "toolUse" ||
            (block as { type?: unknown }).type === "functionCall" ||
            (block as { type?: unknown }).type === "tool_call" ||
            (block as { type?: unknown }).type === "tool_use" ||
            (block as { type?: unknown }).type === "function_call"
          ) {

    A companion test covering sessions_spawn with type tool_use and attachments would confirm the redaction path is now reached for snake_case blocks.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/session-transcript-repair.ts
    Line: 261-297
    
    Comment:
    **Snake_case blocks bypass sanitization and are silently lost in mixed-type messages**
    
    The inner type guard (lines 262–266) was not updated alongside `isRawToolCallBlock`, so it still only matches the three camelCase variants. This creates two concrete problems:
    
    **1. `sessions_spawn` attachment redaction is skipped for `tool_use` blocks.**
    `sanitizeToolCallBlock` — which strips sensitive inline attachment content — is only reached when the inner `if (camelCase)` branch is entered. A `sessions_spawn` call that arrives with Anthropic's native `tool_use` type will pass `isRawToolCallBlock`, survive the drop check (it has `input`), but then fall through the inner guard without ever calling `sanitizeToolCallBlock`. The raw attachment content is persisted to session history un-redacted.
    
    **2. Valid snake_case blocks are silently dropped when any sibling camelCase block has been mutated.**
    If `messageChanged` becomes `true` for another block in the same content array (e.g. a camelCase `toolUse` whose `sessions_spawn` attachments were redacted, or a padded name that was trimmed), the loop exits and the code reaches `out.push({ ...msg, content: nextContent })` — using `nextContent`, which never received the snake_case block. The block disappears from the session without being counted as dropped.
    
    The reason the two new tests pass despite this gap: in a message containing _only_ snake_case blocks no mutation is triggered, so `messageChanged` stays `false` and `droppedInMessage` stays `0`, falling through to `out.push(msg)` which preserves the untouched original `msg`. This is accidentally correct for that narrow case but masks the two failure modes above.
    
    The fix is to add the snake_case types to the inner guard so they receive the same sanitization path:
    
    ```typescript
    // src/agents/session-transcript-repair.ts – lines 262-266
          if (
            (block as { type?: unknown }).type === "toolCall" ||
            (block as { type?: unknown }).type === "toolUse" ||
            (block as { type?: unknown }).type === "functionCall" ||
            (block as { type?: unknown }).type === "tool_call" ||
            (block as { type?: unknown }).type === "tool_use" ||
            (block as { type?: unknown }).type === "function_call"
          ) {
    ```
    
    A companion test covering `sessions_spawn` with type `tool_use` and attachments would confirm the redaction path is now reached for snake_case blocks.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 261-297

Comment:
**Snake_case blocks bypass sanitization and are silently lost in mixed-type messages**

The inner type guard (lines 262–266) was not updated alongside `isRawToolCallBlock`, so it still only matches the three camelCase variants. This creates two concrete problems:

**1. `sessions_spawn` attachment redaction is skipped for `tool_use` blocks.**
`sanitizeToolCallBlock` — which strips sensitive inline attachment content — is only reached when the inner `if (camelCase)` branch is entered. A `sessions_spawn` call that arrives with Anthropic's native `tool_use` type will pass `isRawToolCallBlock`, survive the drop check (it has `input`), but then fall through the inner guard without ever calling `sanitizeToolCallBlock`. The raw attachment content is persisted to session history un-redacted.

**2. Valid snake_case blocks are silently dropped when any sibling camelCase block has been mutated.**
If `messageChanged` becomes `true` for another block in the same content array (e.g. a camelCase `toolUse` whose `sessions_spawn` attachments were redacted, or a padded name that was trimmed), the loop exits and the code reaches `out.push({ ...msg, content: nextContent })` — using `nextContent`, which never received the snake_case block. The block disappears from the session without being counted as dropped.

The reason the two new tests pass despite this gap: in a message containing _only_ snake_case blocks no mutation is triggered, so `messageChanged` stays `false` and `droppedInMessage` stays `0`, falling through to `out.push(msg)` which preserves the untouched original `msg`. This is accidentally correct for that narrow case but masks the two failure modes above.

The fix is to add the snake_case types to the inner guard so they receive the same sanitization path:

```typescript
// src/agents/session-transcript-repair.ts – lines 262-266
      if (
        (block as { type?: unknown }).type === "toolCall" ||
        (block as { type?: unknown }).type === "toolUse" ||
        (block as { type?: unknown }).type === "functionCall" ||
        (block as { type?: unknown }).type === "tool_call" ||
        (block as { type?: unknown }).type === "tool_use" ||
        (block as { type?: unknown }).type === "function_call"
      ) {
```

A companion test covering `sessions_spawn` with type `tool_use` and attachments would confirm the redaction path is now reached for snake_case blocks.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: recognize snake..."

@lishuaigit lishuaigit force-pushed the fix/session-repair-snake-case-tool-types branch from f72c134 to a0084c1 Compare March 20, 2026 07:26
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0084c151c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/session-transcript-repair.ts
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Mar 20, 2026
@lishuaigit lishuaigit force-pushed the fix/session-repair-snake-case-tool-types branch from 68172b8 to 5e9c003 Compare March 30, 2026 03:00
@openclaw-barnacle openclaw-barnacle Bot removed the docs Improvements or additions to documentation label Mar 30, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e9c003df7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/session-transcript-repair.ts
lishuai and others added 4 commits April 28, 2026 07:35
isRawToolCallBlock() only matched camelCase variants (toolCall, toolUse,
functionCall) but Anthropic and some providers persist snake_case types
(tool_call, tool_use, function_call) in session transcripts. When a
streamed tool call is interrupted, the broken block with missing input
survives sanitization because repairToolCallInputs skips it, causing
persistent "session history corrupted" errors.

Add tool_call, tool_use, and function_call to the type check so the
repair logic can detect and fix (or drop) broken snake_case blocks.

Closes openclaw#48915
The inner type guard only checked camelCase block types (toolCall,
toolUse, functionCall), so snake_case blocks (tool_use, tool_call,
function_call) passed isRawToolCallBlock but fell through without
sanitization. This caused sessions_spawn attachments in snake_case
blocks to persist un-redacted.

Add a companion test confirming redaction works for tool_use blocks
with sessions_spawn attachments.
@vincentkoc vincentkoc force-pushed the fix/session-repair-snake-case-tool-types branch from be87849 to 4a1eeab Compare April 28, 2026 07:35
@vincentkoc
Copy link
Copy Markdown
Member

ProjectClownfish pushed a narrow repair to this branch so the original contributor path can stay canonical.

Source PR: #50802
Validation: pnpm test -- src/agents/session-transcript-repair.test.ts; pnpm check:changed
Contributor credit is preserved in the branch history and PR context.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs maintainer review before merge.

Summary
The PR adds snake_case tool-call block support to transcript repair, result-pairing and tool-call ID helpers, with regression tests and a changelog entry.

Reproducibility: yes. from source inspection. A persisted assistant block like { type: "tool_use", id: "call_1", name: "read" } missing input and arguments is ignored by current main's camelCase-only detector, so it bypasses the documented malformed-tool-call drop path before replay.

Next step before merge
The remaining action is maintainer review plus rebase/mergeability and exact-head validation; there is no discrete automated repair finding in the patch.

Security
Cleared: The diff is limited to transcript repair helpers, tests, and changelog text, and it improves sessions_spawn redaction coverage without adding dependencies, workflows, scripts, package metadata, or install surfaces.

Review details

Best possible solution:

Land this PR, or an equivalent main-branch fix, after mergeability is restored and exact-head validation passes; keep linked #48915 open until the implementation merges.

Do we have a high-confidence way to reproduce the issue?

Yes, from source inspection. A persisted assistant block like { type: "tool_use", id: "call_1", name: "read" } missing input and arguments is ignored by current main's camelCase-only detector, so it bypasses the documented malformed-tool-call drop path before replay.

Is this the best way to solve the issue?

Yes. The PR applies the same narrow type-set expansion across detection, rebuild/redaction, result-pairing extraction, ID rewriting, tests, and changelog without adding a provider-specific migration or new policy knob.

Acceptance criteria:

  • pnpm test -- src/agents/session-transcript-repair.test.ts src/agents/tool-call-id.test.ts
  • pnpm check:changed

What I checked:

Likely related people:

  • steipete: GitHub commit history shows repeated recent work in transcript repair, tool-call ID helpers, and replay-history paths, including shared tool-call transcript helpers and strict provider replay fixes. (role: recent maintainer / routing owner; confidence: high; commits: 848f154f3eaa, ea1e933b29a2, 7f6452897e25; files: src/agents/session-transcript-repair.ts, src/agents/tool-call-id.ts, src/agents/pi-embedded-runner/replay-history.ts)
  • vincentkoc: The PR timeline says vincentkoc pushed the current narrow repair branch, and recent history shows adjacent work on sessions_spawn redaction, OpenAI replay IDs, and transcript hygiene docs. (role: PR branch maintainer / adjacent owner; confidence: medium; commits: 4a1eeab0f107, efc3a52947e9, a09e228e3e8e; files: src/agents/session-transcript-repair.ts, src/agents/tool-call-id.ts, docs/reference/transcript-hygiene.md)
  • jalehman: Recent merged history includes the malformed replay tool-call sanitizer work on the same transcript-repair surface, which is directly adjacent to this PR's bug path. (role: adjacent feature-history owner; confidence: medium; commits: c3972982b5d4; files: src/agents/session-transcript-repair.ts)

Remaining risk / open question:

  • The provided PR context reports mergeable: false, so the branch may need rebase or conflict handling before exact-head validation.
  • I did not run tests in this read-only review because pnpm is unavailable in the checkout; validation should still run on the exact PR head before merge.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 103cdd9d96f8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling clawsweeper Tracked by ClawSweeper automation size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: isRawToolCallBlock misses snake_case tool call types (tool_use, tool_call), causing corrupted session history errors with Anthropic

2 participants