May 11, 2026 β€’ Version: v2026.5.8

Codex thread/start Validation Fails When Thread Response Omits sessionId

OpenClaw v2026.5.8 rejects valid Codex app-server thread/start responses due to schema mismatch where Thread objects may lack sessionId after app-server schema sync.

πŸ” Symptoms

The gateway fails immediately after initiating a Codex thread via the app-server protocol, before any prompt is dispatched. The following error surfaces in the diagnostic log:

[agents/harness] Codex agent harness failed; not falling back to embedded PI backend
[diagnostic] lane task error: lane=main error="Error: Invalid Codex app-server thread/start response: data/thread must have required property 'sessionId'"
Embedded agent failed before reply: Invalid Codex app-server thread/start response: data/thread must have required property 'sessionId'

Technical Manifestations:

  • Failure Point: POST /thread/start response parsing in agents/harness module
  • Exit Condition: Gateway marks Codex harness as failed, does not fall back to embedded PI backend
  • HTTP Context: The request may complete successfully on the Codex app-server side; failure occurs during OpenClaw's response validation
  • Telemetry Gap: No prompt metrics emitted because the thread initialization fails

Diagnostic Query:

grep -r "must have required property" /var/log/openclaw/ 2>/dev/null || journalctl -u openclaw --since "5 minutes ago" | grep -i "sessionId"

🧠 Root Cause

Sequence of Events:

  1. PR #79152 synchronized OpenClaw's generated schemas from @openai/[email protected]
  2. The new Thread schema promotes sessionId from optional to required
  3. At least one live Codex app-server endpoint still returns { id: "thread-abc123" } without sessionId
  4. OpenClaw's JSON Schema validator rejects the response before it reaches business logic

Architectural Inconsistency:

The Codex app-server protocol boundary exhibits a response shape mismatch:

OpenClaw VersionThread.sessionIdThread.id
v2026.5.7 (stable)OptionalRequired
v2026.5.8+ / mainRequiredRequired
Live Codex App-ServersessionIdid
Some endpointsMissingPresent
Other endpointsPresentPresent

Code Path Analysis:

The validation failure occurs in the agent harness layer:

// agents/harness/codex-harness.ts
const validatedResponse = ThreadSchema.parse(rawResponse);
// If rawResponse.thread lacks sessionId, parse() throws ZodError
// Error propagates as: "Invalid Codex app-server thread/start response:
// data/thread must have required property 'sessionId'"

Schema Divergence:

// schema: Thread (v2026.5.8+)
{
  id: string,           // required
  sessionId: string,    // required ← NEW REQUIREMENT
  createdAt?: string,
  metadata?: object
}

// Live Codex response sometimes returns:
{
  id: "thread-abc123"   // Only id present
}

πŸ› οΈ Step-by-Step Fix

Apply response normalization at the app-server protocol boundary before schema validation.

File to Modify: packages/openclaw-core/src/protocol/codex/normalizers.ts

Before (failing code):

import { ThreadSchema } from './generated-schemas';

export function handleThreadStartResponse(raw: unknown): Thread {
  // Direct parse fails if sessionId missing
  return ThreadSchema.parse(raw);
}

After (corrected code):

import { ThreadSchema } from './generated-schemas';

export function normalizeThreadResponse(raw: unknown): unknown {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'thread' in raw &&
    typeof raw.thread === 'object' &&
    raw.thread !== null
  ) {
    const thread = raw.thread as Record;

    // If id exists but sessionId is missing, promote id to sessionId
    if ('id' in thread && !('sessionId' in thread)) {
      thread.sessionId = thread.id;
    }

    // If sessionId exists but id is missing, promote sessionId to id
    if ('sessionId' in thread && !('id' in thread)) {
      thread.id = thread.sessionId;
    }
  }
  return raw;
}

export function handleThreadStartResponse(raw: unknown): Thread {
  // Normalize before validation
  const normalized = normalizeThreadResponse(raw);
  return ThreadSchema.parse(normalized);
}

Multi-Stage CLI Fix (if hot-patching):

# 1. Locate the normalizer file
find /opt/openclaw -name "normalizers.ts" -path "*/codex/*"

# 2. Apply the normalization patch
cat > /tmp/codex-normalizer.patch << 'EOF'
--- a/packages/openclaw-core/src/protocol/codex/normalizers.ts
+++ b/packages/openclaw-core/src/protocol/codex/normalizers.ts
@@ -1,8 +1,25 @@
 import { ThreadSchema } from './generated-schemas';
 
+function normalizeThreadResponse(raw: unknown): unknown {
+  if (typeof raw === 'object' && raw !== null && 'thread' in raw) {
+    const thread = (raw as any).thread;
+    if (typeof thread === 'object' && thread !== null) {
+      if ('id' in thread && !('sessionId' in thread)) {
+        thread.sessionId = thread.id;
+      }
+      if ('sessionId' in thread && !('id' in thread)) {
+        thread.id = thread.sessionId;
+      }
+    }
+  }
+  return raw;
+}
+
 export function handleThreadStartResponse(raw: unknown): Thread {
-  return ThreadSchema.parse(raw);
+  return ThreadSchema.parse(normalizeThreadResponse(raw));
 }
EOF

# 3. Apply (requires restart)
sudo patch /opt/openclaw/packages/openclaw-core/src/protocol/codex/normalizers.ts < /tmp/codex-normalizer.patch
sudo systemctl restart openclaw

πŸ§ͺ Verification

Unit Test Verification:

// normalizers.test.ts
import { normalizeThreadResponse } from './normalizers';

describe('normalizeThreadResponse', () => {
  it('should handle thread with only id (live Codex behavior)', () => {
    const input = { thread: { id: 'thread-abc123' } };
    const result = normalizeThreadResponse(input);
    expect(result.thread.sessionId).toBe('thread-abc123');
    expect(result.thread.id).toBe('thread-abc123');
  });

  it('should handle thread with only sessionId', () => {
    const input = { thread: { sessionId: 'session-xyz789' } };
    const result = normalizeThreadResponse(input);
    expect(result.thread.id).toBe('session-xyz789');
    expect(result.thread.sessionId).toBe('session-xyz789');
  });

  it('should preserve threads with both fields', () => {
    const input = { thread: { id: 'id-val', sessionId: 'session-val' } };
    const result = normalizeThreadResponse(input);
    expect(result.thread.id).toBe('id-val');
    expect(result.thread.sessionId).toBe('session-val');
  });
});

Integration Test:

# Start a mock Codex app-server returning id-only threads
npm run test:integration -- --suite codex-thread-start

# Expected output:
# βœ“ thread/start returns thread with only id
# βœ“ thread/start returns thread with only sessionId
# βœ“ thread/start returns thread with both fields

End-to-End Verification:

# 1. Ensure OpenClaw is running
sudo systemctl status openclaw --no-pager

# 2. Trigger a Codex thread creation via CLI
openclaw threads create --agent codex --model opus

# Expected: Thread created successfully
# Thread ID: thread-abc123
# Session ID: thread-abc123

# 3. Verify no validation errors in logs
journalctl -u openclaw --since "1 minute ago" | grep -E "(sessionId|thread/start|error)"

Expected Log Output (fixed):

[agents/harness] Codex agent harness initialized
[protocol] thread/start response normalized: id=thread-abc123 sessionId=thread-abc123
[agents/harness] Thread started successfully: id=thread-abc123

⚠️ Common Pitfalls

  • Race Condition in Hot Reload: If OpenClaw supports dynamic config reload, the normalizer must be loaded before any in-flight requests. Apply the fix during a maintenance window or drain connections first.
  • Mutation Side Effects: The normalization mutates the raw response object directly. If downstream code retains references to the original object, this may cause subtle bugs. Consider deep cloning:
    const normalized = JSON.parse(JSON.stringify(raw));
    // then apply normalization
  • TypeScript Strictness: The cast as Record<string, unknown> bypasses type safety. If the generated schema changes, this may silently pass invalid shapes.
  • Docker Layer Caching: If building Docker images, ensure npm install pulls the corrected normalizers.ts. Invalidate build cache:
    docker build --no-cache -t openclaw:patched .
  • macOS Development Environment: The journalctl commands in verification steps require Linux. On macOS, check logs via:
    tail -f ~/Library/Logs/OpenClaw/openclaw.log
  • Windows Subsystem for Linux (WSL): Ensure the WSL kernel version supports systemd for journalctl, or use file-based logging.
  • Version Pinning: If using @openai/codex directly, pin to a known-compatible version. The fix ensures backward compatibility, but isolating the root dependency prevents future regressions.
  • Schema Regeneration: If regenerating schemas via openapi-typescript-codegen, ensure the post-generation hook applies the normalization automatically.
Error Code / PatternDescriptionConnection
must have required property ‘sessionId’Primary symptom; schema validation failureDirect manifestation of this issue
Codex agent harness failedHarness marks agent as unavailableDownstream effect; triggers fallback
not falling back to embedded PI backendFallback logic bypassedSecondary symptom; gateway topology issue
Invalid Codex app-server thread/start responseGeneric wrapper errorThis error’s parent
ZodErrorSchema parsing exception typeRoot exception; thrown by ThreadSchema.parse()
Thread schema mismatchSchema divergence between OpenClaw and CodexHistorical: existed in earlier versions but masked

Historical Context:

  • PR #79152 introduced the schema change requiring sessionId
  • Issue #79153 (parent) tracks the broader Codex protocol compatibility effort
  • v2026.5.7 represents the last stable version without this requirement
  • @openai/[email protected] is the schema source that introduced the incompatibility

Related Documentation:

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.