NousResearch/hermes-agent · error · CodexAppServerError
-32603
-32603
Error message
codex thread/start returned no thread id (payload keys: {sorted(result.keys())}) What it means
CodexAppServerError (code -32603, internal error) from the thread/start handling in agent/transports/codex_app_server_session.py:359. The session layer tolerates several codex serialization variants (thread.id, thread.sessionId, top-level sessionId, threadId) but if none of them yields an id it refuses to continue, reporting which keys the payload actually had so the mismatch is diagnosable.
Solutions
- Read the payload keys in the message and compare against the four tolerated keys (id/sessionId/threadId) to confirm the schema moved.
- Pin the codex app-server to a version this client is tested against (or upgrade hermes-agent to a release supporting the new schema).
- If you maintain a fork, add the new key to the cross-fill chain in codex_app_server_session.py and upstream it.
- Check codex's own logs for why thread/start may have returned a degenerate payload (e.g. auth/config error).
Example fix
# before (tolerated keys only)
thread_id = (
thread_obj.get("id") or thread_obj.get("sessionId")
or result.get("sessionId") or result.get("threadId")
)
# after (extend for a newer codex schema)
thread_id = (
thread_obj.get("id") or thread_obj.get("sessionId")
or result.get("sessionId") or result.get("threadId")
or result.get("conversationId")
) Defensive patterns
Strategy: try-catch
Validate before calling
_TOLERATED_KEYS = ("thread", "sessionId", "threadId")
def thread_start_payload_looks_ok(result: dict) -> bool:
t = result.get("thread") or {}
return bool(t.get("id") or t.get("sessionId") or result.get("sessionId") or result.get("threadId")) Try / catch
from agent.transports.codex_app_server import CodexAppServerError
try:
session.start_thread()
except CodexAppServerError as exc:
if exc.code == -32603 and "no thread id" in exc.message:
raise RuntimeError(
f"codex schema mismatch (keys: {exc.message}); pin/upgrade codex"
) from exc
raise Prevention
- Pin codex to a version tested with this client.
- Include the payload-keys detail from the message in bug reports.
- After any codex upgrade, smoke-test thread/start before rolling out.
When it happens
Trigger: A codex app-server version whose thread/start response schema moved the thread id to a new key not in the tolerated list (or omits it entirely on failure), typically right after upgrading/downgrading codex.
Common situations: Version skew: the pinned codex version changed its JSON-RPC payload shape; a partially-failed thread/start that returns an error-ish dict without an id; nightly/rolling codex installs.
Related errors
- already initialized
- codex app-server client is closed
- codex app-server error
- codex app-server method
- codex app-server stdin not available
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/cb98b1f895871c78.
Report an issue: GitHub.
Appendix: source
Thrown at agent/transports/codex_app_server_session.py:359
# configured otherwise in their codex config.toml) is the standard
# codex CLI workflow and avoids fighting codex's own validation.
# Users who want a write-capable profile configure it in their
# ~/.codex/config.toml the same way they would for any codex usage.
params: dict[str, Any] = {"cwd": self._cwd}
result = self._client.request("thread/start", params, timeout=15)
# Cross-fill thread.id/sessionId — different codex versions have
# serialized this under either key. Mirrors openclaw beta.8's
# tolerance fix so future codex drops/renames don't KeyError us
# at handshake time.
thread_obj = result.get("thread") or {}
thread_id = (
thread_obj.get("id")
or thread_obj.get("sessionId")
or result.get("sessionId")
or result.get("threadId")
)
if not thread_id:
raise CodexAppServerError(
code=-32603,
message=(
"codex thread/start returned no thread id "
f"(payload keys: {sorted(result.keys())})"
),
)
self._thread_id = thread_id
logger.info(
"codex app-server thread started: id=%s profile=%s cwd=%s",
self._thread_id[:8],
self._permission_profile,
self._cwd,
)
return self._thread_id
def close(self) -> None:
if self._closed:
returnView on GitHub (pinned to c896c09c42)