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/startresponse parsing inagents/harnessmodule - 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:
- PR
#79152synchronized OpenClaw's generated schemas from@openai/codex@0.129.0 - The new
Threadschema promotessessionIdfrom optional to required - At least one live Codex app-server endpoint still returns
{ id: "thread-abc123" }withoutsessionId - 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 Version | Thread.sessionId | Thread.id |
|---|---|---|
| v2026.5.7 (stable) | Optional | Required |
| v2026.5.8+ / main | Required | Required |
| Live Codex App-Server | sessionId | id |
|---|---|---|
| Some endpoints | Missing | Present |
| Other endpoints | Present | Present |
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 fieldsEnd-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 installpulls the correctednormalizers.ts. Invalidate build cache:docker build --no-cache -t openclaw:patched . - macOS Development Environment: The
journalctlcommands 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
systemdfor journalctl, or use file-based logging. - Version Pinning: If using
@openai/codexdirectly, 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.
π Related Errors
| Error Code / Pattern | Description | Connection |
|---|---|---|
must have required property ‘sessionId’ | Primary symptom; schema validation failure | Direct manifestation of this issue |
Codex agent harness failed | Harness marks agent as unavailable | Downstream effect; triggers fallback |
not falling back to embedded PI backend | Fallback logic bypassed | Secondary symptom; gateway topology issue |
Invalid Codex app-server thread/start response | Generic wrapper error | This error’s parent |
ZodError | Schema parsing exception type | Root exception; thrown by ThreadSchema.parse() |
Thread schema mismatch | Schema divergence between OpenClaw and Codex | Historical: 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/codex@0.129.0 is the schema source that introduced the incompatibility
Related Documentation: