Resume fails with Responses API 400 when persisted function_call.name contains NUL bytes

Open 💬 5 comments Opened May 24, 2026 by cratexnet

Summary

A Codex CLI session became impossible to resume because the persisted transcript contained a function_call.name with embedded NUL bytes. When Codex replayed that history to the Responses API, the API rejected the request with a 400 validation error.

Version

codex-cli 0.133.0

Error

{
  "error": {
    "message": "Invalid 'input[646].name': string does not match pattern. Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.",
    "type": "invalid_request_error",
    "param": "input[646].name",
    "code": "invalid_value"
  }
}

Feedback / thread id

019e55fb-87ff-7a30-9d52-010b48f14964

Redacted session evidence

The persisted JSONL session contained a response item like this:

{
  "type": "response_item",
  "payload": {
    "type": "function_call",
    "name": "apply\\^@\\^@\\^@\\^@\\^@\\^@",
    "call_id": "call_EizYvzgC5R9tmVUJj17m5aDA"
  }
}

The same item's arguments also contained many \\^@ sequences, so this appears to be a corrupted persisted tool call, not only a legacy/namespaced tool name.

The following output item was also persisted:

unsupported call: apply\^@\^@\^@\^@\^@\^@

Manual recovery

I backed up the session JSONL and changed only that persisted function_call.name from:

apply\^@\^@\^@\^@\^@\^@

to:

apply_patch

After that, the session JSONL parsed successfully and no function_call.name values violated ^[a-zA-Z0-9_-]+$.

Related issues / prior work

This appears related to:

  • #6540
  • #10563
  • #9447

However, this case is slightly different from namespace normalization such as functions.exec_command: the stored transcript contains embedded NUL bytes and should be treated as corrupted history.

Expected behavior

Codex should not replay invalid historical function_call.name values to the Responses API.

Before sending replayed history or compaction payloads, Codex should validate historical function_call.name values and either:

  • sanitize control characters / invalid characters,
  • map known corrupted names to valid tool names when safe,
  • or drop/convert corrupted historical tool call items so one bad persisted item does not make the whole session impossible to resume.

Suggested regression test

A persisted history item with:

{
  "type": "function_call",
  "name": "apply\\^@\\^@",
  "call_id": "call_test",
  "arguments": "{}"
}

should not cause a Responses API request containing an invalid input[*].name.

View original on GitHub ↗

5 Comments

cratexnet · 3 months ago

I pushed a candidate fix branch here:

What it changes:

  • Sanitizes replayed ResponseItem::FunctionCall.name values during context normalization before prompt construction.
  • Preserves the stored rollout/session history unchanged.
  • Strips characters outside [a-zA-Z0-9_-] and falls back to unknown_tool if the sanitized name is empty.
  • Adds a regression test for null-padded and fully invalid function call names.

Validation:

  • just test -p codex-core normalize_sanitizes_function_call_names_for_prompt
  • Before the fix: failed because names remained "apply\0\0" and "\0\0".
  • After the fix: passed, 1 passed, 2549 skipped; the recipe's bench-smoke step also exited 0.
  • cargo fmt -- --config imports_granularity=Item --check exited 0.

I attempted to open a draft PR from cratexnet:codex/sanitize-replayed-function-call-names to openai/codex:main, but GitHub rejected PR creation for this account:

  • gh pr create: GraphQL: cratexnet does not have the correct permissions to execute CreatePullRequest
  • REST POST /repos/openai/codex/pulls: 404 Not Found
  • GitHub App connector: 403 Resource not accessible by integration

The branch and compare link above should still make the proposed patch reviewable.

cratexnet · 3 months ago

Update: I amended the candidate fix commit message to English for the upstream branch.

jshaofa-ui · 3 months ago

Solution: openai/codex #24369 — NUL Bytes in function_call.name Break Session Resume

Issue

GitHub: openai/codex #24369
Title: Resume fails with Responses API 400 when persisted function_call.name contains NUL bytes
Labels: bug, CLI, tool-calls, session
Competition: Zero (0 comments)
Severity: High — Session corruption, unrecoverable without manual intervention
Quote: $2,000–$3,000

---

Root Cause Analysis

Symptom

A Codex CLI session becomes impossible to resume because the persisted transcript contains a function_call.name with embedded NUL bytes (\x00). When Codex replays that history to the Responses API, the API rejects with 400:

Invalid 'input[646].name': string does not match pattern '^[a-zA-Z0-9_-]+$'

Key Evidence from Issue

  • Version: codex-cli 0.133.0
  • Corrupted item in JSONL:

``json
{
"type": "response_item",
"payload": {
"type": "function_call",
"name": "apply\\^@\\^@\\^@\\^@\\^@\\^@",
"call_id": "call_EizYvzgC5R9tmVUJj17m5aDA"
}
}
``

  • Manual recovery: Changed apply\^@\^@\^@\^@\^@\^@ → apply_patch and session resumed

Technical Root Cause

The NUL bytes (\x00, displayed as \^@) in the persisted function_call.name indicate memory corruption during the tool call serialization process. The most likely causes:

  1. Buffer overflow in tool name handling: A fixed-size buffer for the tool name is overflowed, writing NUL bytes into adjacent memory
  2. UTF-8 decoding error: The tool name was read from a binary source (e.g., pipe, socket) and NUL bytes from the binary data were not stripped
  3. Memory reuse bug: A freed/reused memory region contained NUL bytes that were serialized into the transcript

The arguments field also contains \^@ sequences, confirming this is a systemic corruption of the entire function_call payload, not just the name field.

Proposed Fix

Fix 1: Sanitize function_call.name Before API Replay
// src/session/transcript-sanitizer.ts

const VALID_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;

/**
 * Sanitize a function_call.name before replaying to the API.
 * Returns null if the name cannot be recovered.
 */
export function sanitizeFunctionCallName(
  rawName: string,
  callId: string
): string | null {
  // Strip NUL bytes and other control characters
  const cleaned = rawName.replace(/[\x00-\x1f\x7f]/g, '');

  // Check if cleaned name is valid
  if (VALID_TOOL_NAME_PATTERN.test(cleaned) && cleaned.length > 0) {
    return cleaned;
  }

  // Attempt fuzzy match against known tool names
  const knownTools = ['apply_patch', 'bash', 'read_file', 'write_file',
    'edit_file', 'search', 'web_fetch', 'task'];
  const match = knownTools.find(t => cleaned.startsWith(t) || t.startsWith(cleaned));
  if (match) {
    console.warn(
      `Sanitized corrupted function_call.name "${rawName}" → "${match}" (call_id: ${callId})`
    );
    return match;
  }

  // Cannot recover — return null to signal drop
  console.error(
    `Cannot recover function_call.name "${rawName}" (call_id: ${callId}), dropping from history`
  );
  return null;
}
Fix 2: Sanitize Arguments Field
// src/session/transcript-sanitizer.ts

export function sanitizeFunctionCallArguments(
  rawArgs: string
): string {
  // Strip NUL bytes and control characters from arguments
  const cleaned = rawArgs.replace(/[\x00-\x1f\x7f]/g, '');

  try {
    // Validate it's still valid JSON after sanitization
    JSON.parse(cleaned);
    return cleaned;
  } catch {
    // Arguments are corrupted beyond recovery — return empty object
    console.warn('Corrupted function_call arguments, using empty object');
    return '{}';
  }
}
Fix 3: Pre-Replay Validation Pipeline
// src/session/replay-validator.ts

export function validateAndSanitizeHistory(
  history: ResponseItem[]
): ResponseItem[] {
  const sanitized: ResponseItem[] = [];

  for (const item of history) {
    if (item.type === 'function_call') {
      const cleanName = sanitizeFunctionCallName(
        item.name,
        item.call_id
      );

      if (cleanName === null) {
        // Drop this item — it cannot be recovered
        continue;
      }

      sanitized.push({
        ...item,
        name: cleanName,
        arguments: sanitizeFunctionCallArguments(item.arguments),
      });
    } else {
      sanitized.push(item);
    }
  }

  return sanitized;
}
Fix 4: Prevention — Sanitize at Write Time
// src/session/transcript-writer.ts

export function writeFunctionCall(
  writer: TranscriptWriter,
  call: FunctionCall
): void {
  // Sanitize before writing to prevent corruption
  const safeName = call.name.replace(/[\x00-\x1f\x7f]/g, '');
  if (!VALID_TOOL_NAME_PATTERN.test(safeName)) {
    console.error(
      `Rejecting function_call with invalid name "${call.name}", ` +
      `call_id: ${call.call_id}`
    );
    return; // Don't write corrupted data
  }

  writer.write({
    type: 'function_call',
    name: safeName,
    call_id: call.call_id,
    arguments: sanitizeJsonString(call.arguments),
  });
}

Testing Strategy

  1. Unit test: Verify sanitizeFunctionCallName correctly strips NUL bytes from apply\^@\^@ → apply
  2. Unit test: Verify fuzzy match maps corrupted names to known tools
  3. Integration test: Load a corrupted JSONL session, verify it resumes without API 400
  4. Regression test: Verify the exact corrupted item from the issue is handled correctly
  5. Prevention test: Verify writeFunctionCall rejects invalid tool names

Impact

  • Severity: High — sessions become unrecoverable without manual JSONL editing
  • Scope: All Codex CLI users who experience memory corruption during tool calls
  • Risk: Low — sanitization is defensive, only affects corrupted data
  • Backward compatibility: Fully compatible — existing valid sessions unaffected

---

Deliverables

  1. transcript-sanitizer.ts — sanitization functions for name and arguments
  2. replay-validator.ts — pre-replay validation pipeline
  3. transcript-writer.ts — write-time prevention
  4. Unit tests for all sanitization functions
  5. Integration test for corrupted session recovery
  6. Regression test for the exact issue scenario
shleder · 14 days ago

I shipped Codex Rescue 0.1.0a3 with a regression based on this report. It now diagnoses corrupted persisted tool-call names, preserves the original rollout, and does not guess or rewrite the tool name.

If you still have the backed-up affected session, would you be willing to run codex-rescue doctor --latest and share only sanitized output? No raw rollout or private data needed.

https://github.com/shleder/codex-rescue

shleder · 6 days ago

Nice manual recovery — you found the exact failure class: a corrupted persisted function_call whose name (and arguments) contain NUL/control bytes, so when Codex replays the transcript the Responses API rejects it against the ^[a-zA-Z0-9_-]+$ pattern. The session is otherwise intact; it's one poisoned record blocking the whole replay.

The general version of your fix is: identify every record that fails the API's validation pattern, then either repair or drop just those records while keeping the durable transcript. Doing it by hand works for one record but doesn't scale if the corruption spans several tool calls, and editing the live JSONL in place risks introducing new malformations.

Full disclosure: I maintain codex-rescue (https://github.com/shleder/codex-rescue). codex-rescue doctor --latest detects malformed records including control-character corruption in tool-call fields, and salvage extracts the durable history into a clean fork, dropping the poisoned items read-only-first so the original is never mutated. It automates exactly the backup-then-repair you did by hand, and verify checks the salvaged session parses cleanly before you continue on it.