Skip to article
HB CodeDocsGet the app

HandbookArchitecture

Two agents, one chat layer

How Codex and Claude fit into one chat layer: what the shared layer owns, what each adapter owns, what the dispatch layer decides, and where the two agents differ in accounts, access levels, and abilities.

Checked against the source on 30 September 2026

On this page

Codex and Claude side by side

HB Code runs two coding agents. Codex runs through the Codex app-server, a child process that speaks JSON-RPC. Claude runs through the agent host, a Node process that runs the Claude Agent SDK. Both agents feed one chat layer: one outbox, one run registry, one set of timeline rows, one approval card, one tool catalog, and one RPC contract for the phones. A session uses one agent for its whole life.

The largest difference is how long things live. The figure shows two runs of one session and a settings change between them.

The Codex child and its tool list outlive a run. A Claude host and its tool list live for one run.
The Codex child and its tool list outlive a run. A Claude host and its tool list live for one run.A timeline that is not to scale shows two runs of one session with a pause between them. In the pause, the user saves a Gemini API key, which adds two tools to the HB Code tool catalog. The top lane is the shared chat run. In a Codex session, the app-server child stays running through both runs and the pause, and the thread keeps the tool list that thread/start gave it, so run 2 still has the old list. In a Claude session, each run starts a new agent host that ends with the run, and no Claude process exists in the pause. Each open sends the current tool list, so run 2 has the two new tools.
Chat runshared chat layer
Codex app-serverchild process
Codex threadits tool list
Claude agent hostNode process
Claude tool listsent with open
Gemini API key saved: 2 more tools in the catalog
run 1
run 2
one child, resident between runs
one list, fixed at thread/start
run 2 still has the old list
host 1
host 2
no Claude process
list without the Gemini tools
list with the Gemini tools
run 1 starts
run 1 ends
run 2 starts
run 2 ends
  • Chat runchat/runs

    Shared by both agents. A session has one active run at a time, and the outbox starts the next run after the first one settles.

  • Codex app-serverMAX_LIVE_APP_SERVERS = 3

    One child per profile pair serves many sessions and many runs. At most 3 idle children stay resident, and the registry grows when all of them are busy.

  • Codex threadthread/start · dynamicTools

    Gets the tool list once. thread/resume has no field for tools, so a new setting reaches only a thread that starts later.

  • Claude agent hostopen · appCapabilities

    One Node process per run. The open request carries the tool list of that moment, the model, the effort, and the permission mode.

TopicCodexClaude
Process and lifetimeThe codex-app-server child process. One child per profile pair serves many sessions. At most 3 idle children stay resident, and the registry grows when all of them are busy.The agent host, a Node process that starts the claude executable. Each run gets a new host, and the host ends with its run. Reads such as the model catalog, a title, or a history page run in a one-shot host.
ProtocolJSON-RPC lines over stdin and stdout: requests, notifications, and requests from the app-server to the desktop.One JSON object per line over stdin and stdout. A request carries an id, and a line without an id is an event. The desktop gives each request 35 seconds.
Where history comes fromthread/items/list over the pipe. The desktop never reads the Codex files.The host reads the saved transcript with the SDK call getSessionMessages, in pages of at most 100 messages. The desktop never reads the Claude files.
When a message counts as sentThe thread history holds a userMessage whose clientId equals the outbox operationId.The saved transcript holds the message UUID that the desktop saved for the operation in claude_operations.
Tool list lifetimeSent as dynamicTools with thread/start. The thread keeps that list for its whole life.Sent as appCapabilities with each open. Each run gets the list that the settings give at that moment.
ApprovalsThe app-server sends a request to the desktop. The access level sets the sandbox and the approval policy.The SDK calls canUseTool inside the host. The host holds the call open and sends a permission event.
AccountsCodex auth profiles. The active profile pays for the next turn.The default account in Claude’s own folder and added account folders. The active account pays for the next run.
Subagents or child threadsChild threads of the root thread. The desktop follows an open child thread live through a watch.Subagents of the run, which a workflow can start in groups. Their turns arrive through the parent’s host, and a subagent takes no direct input.
What HB Code storescodex_session_links: the thread ID, the profile, and the rollout path as an opaque value.claude_sessions: the Claude session ID of the session. claude_operations: one message UUID per outbox operation.

The layer rule: chat, dispatch, adapters

The Rust code has one rule for the two agents. The shared chat layer in desktop/src-tauri/src/chat never names an adapter. When a step needs the agent, chat calls agents/dispatch. dispatch reads the session’s agent and calls agents/codex or agents/claude. The two adapters never import each other.

The chat layer reaches an adapter only through agents/dispatch, and the adapters never reach each other
The chat layer reaches an adapter only through agents/dispatch, and the adapters never reach each otherA map of four code areas in desktop/src-tauri/src. At the top, the shared chat layer in chat/ takes calls from the Tauri commands and the phone RPC. One arrow goes from chat/ down into agents/dispatch/, which holds six decision files: send.rs, selection.rs, runs.rs, approvals.rs, conversations.rs, and timeline.rs. Each file shows its rule: by the permit’s agent, by the session’s agent, or Claude first and then Codex. From dispatch, one arrow goes to agents/codex/ and one to agents/claude/. Two crossed stubs below chat/ show that chat never calls an adapter directly. A dashed wall with a cross between the two adapters shows that they never import each other. Two soft arrows go from the adapters up to chat/, because both use its shared types. On the right, usage_history/ reads both adapters directly.
chat/agent-neutral: outbox, run registry, timeline rows, approvals, session messages
CallersTauri commands · phone RPC
every agent step
no direct call
no direct call
agents/dispatch/reads the session’s agent, picks the adapter
send.rsby the permit’s agent
selection.rsby the session’s agent
runs.rsClaude first, then Codex
approvals.rsClaude first, then Codex
conversations.rsby the session’s agent
timeline.rsby the session’s agent
agents/codex/app-server child, turns, history, auth profiles
agents/claude/agent host, run loop, projection, accounts
uses the shared types
uses the shared types
no import in either direction
usage_history/saves limit readings of both account types
reads both adapters
  • chat/desktop/src-tauri/src/chat

    The agent-neutral layer. No file in it names agents::codex or agents::claude. It calls agents/dispatch when a step needs the agent.

  • agents/dispatch/7 files · mod.rs: session()

    Reads the session row and picks the adapter. Both adapters also call its session lookup, and nothing else in it.

  • agents/codex/ and agents/claude/two adapters

    Each adapter runs its own engine and maps it to the chat contract. Neither one names the other.

  • usage_history/mod.rs · recording.rs

    The second place that names both adapters. It reads the active Codex profile and the active Claude account, because it saves limit readings of both.

Both adapters use the shared types and services of chat, for example the send request, the timeline rows, and the run registry. Both also call one function of dispatch, the session lookup. usage_history is the second module that names both adapters, because it saves the limit readings of both account types. Code that serves one agent names that adapter directly. The HTML document worker in services/agent_tools is an example: it leases a Codex app-server.

What the dispatch layer decides

agents/dispatch has seven files. Most functions read the session row and match on its agent. Some functions have a different rule, because their caller has no session agent at hand or because only one adapter keeps the state.

FileWhat it decidesWho answers
mod.rsThe session lookup by base session ID. Every other file starts here.A missing row fails with “The chat session does not exist.”
selection.rsThe model catalog for the picker, the first model and effort of a session, and the check of model and effort before a send.The session’s agent. The check returns a permit for that agent. A Claude permit can’t move to a fresh thread after a missing thread link.
send.rsThe three ability flags, the send itself, the history check of one uncertain operation, the active turn, the release of an accepted steer for a replay, the rollback of the latest turn, and the removal of a dead thread link.The send follows the permit: a Claude permit goes to the Claude adapter, and without a permit only a Codex steer continues. The history check follows the session’s agent. The other four are Codex only.
runs.rsQuestions of the run registry: does an agent turn still own this run, may a stale run be reclaimed, cancel a run, and watch a child thread.Turn ownership is always the Codex adapter, which keeps no records for a Claude session. A registered Codex turn or a live Claude host blocks a reclaim. For a cancel, Claude answers first, then Codex. A Claude session gets no child thread watch.
approvals.rsThe answer to an approval card. The resolve call names a request, a thread, a turn, and an item, and no session.Claude answers first when it holds a request with this id. Otherwise the Codex app-server that owns the request settles it.
conversations.rsThe agent’s own conversation ID, the list and the preview of saved conversations, the session title, the session that already owns a conversation, and the link of a new or a duplicated session.The session’s agent, or the agent in the request. A Claude list needs a project directory. Codex writes titles with gpt-5.6-luna, and Claude with haiku.
timeline.rsThe latest window, an older page, a child thread and its pages, the list of agent threads with the workflows that started them, and command output.The session’s agent. For Claude, a child thread is a subagent, and only Claude fills the workflows list. Codex returns an empty one. Output that the desktop stored as an artifact exists only for Codex runs. Claude output comes from the host call toolOutput and goes through the shared range reader.

A session picks its agent when it is created

session.create carries agent and an optional conversationId. agent is codex or claude, and the request type has no default for it, so a request without agent fails to decode. conversationId names a saved conversation of that agent: a Codex thread ID or a Claude session ID.

  1. When a session already continues that conversation, create returns that session. A conversation has one owner.
  2. Otherwise the desktop saves the session row with chat_agent and with pending chat settings.
  3. The adapter links the session. Codex links the chosen thread. Claude writes a row in claude_sessions with the chosen session ID, or with a new UUID for a new conversation.
  4. When the link fails, the desktop deletes the new session and returns the error.

The column chat_agent accepts only codex and claude, and no code changes it after the insert. A session therefore never changes its agent. A duplicated session keeps the agent and starts its own conversation: Claude gets a new session ID at once, and Codex starts a new thread at the first send.

The desktop remembers the last choice in the WebView’s local storage under plantocode:new-session-agent:v1 and preselects it in the next dialog. Without a saved value it preselects Codex. The value is a preselection, and the create request always names the agent.

One access level, two mechanisms

A message carries one of four HB Code access levels. Each adapter maps the level to the agent’s own mechanism. Codex gets a permission profile and an approval policy. Claude gets one of its permission modes. The interface shows each agent’s own name for the level.

Access levelCodex: name, permissions, approvalPolicyClaude: name, permissionMode
read-onlyRead only. :read-only, on-requestPlan mode. plan
full-autoFull auto. :workspace, on-requestAccept edits. acceptEdits
full-accessFull access. :danger-full-access, on-requestAuto mode. auto
unrestrictedNever ask. :danger-full-access, neverBypass permissions. bypassPermissions

Codex receives the pair with thread/start and again with each turn/start, so a new level applies to the next turn. An ephemeral Codex request always runs with :read-only and never. Claude receives the mode with the open request of each run, so a new level applies to the next run. For auto, the host asks the SDK whether the chosen model supports Auto mode. When it doesn’t, the open fails and the run doesn’t start.

What each agent can do

The submit service asks agents/dispatch for three flags before it saves a message: steer, replace_latest, and speed_modes. Codex has all three, and Claude has none. A request that needs a missing ability fails at submit with a validation error, so it never enters the outbox. The other differences live in the adapters.

AbilityCodexClaude
Steer a running turnYes. The message joins the active turn.No. Submit rejects the steer. The message waits in the queue, or it goes after you stop the run.
Replace the latest sent messageYes. The desktop rolls back the latest turn and sends the edited message.No. Submit rejects the replacement.
Speed modeYes. Fast sends serviceTier priority.No. Submit rejects every mode other than standard.
Stop and send nowYes. It takes back an accepted steer and sends it as a new turn.No. A Claude session has no accepted steer to take back.
GoalsYes. One run can hold many turns.No. The call fails with “Goals are available in Codex sessions.”
HB Code toolsThe same catalog. Codex runs these tools without asking.The same catalog. maintain_sessions runs without asking, send_session_message follows the desktop’s own rule, and every other tool follows the access level.
HTML document workerA Codex turn on the session’s app-server, with the settings of the calling turn.The same Codex worker. The tool call leases a Codex app-server, so the document is written by Codex also in a Claude session.
Child threadsA child thread can accept direct input when Codex reports canAcceptDirectInput.A subagent never accepts direct input.
Command outputThe desktop stores live output as an artifact, and reads saved output from the app-server.The host reads saved output on demand with toolOutput.
Continue a saved conversationYes. The project directory is optional in the list request.Yes. The list request needs a project directory.

Accounts, limits, and usage history

Each agent has its own accounts and its own plan limits. Codex has auth profiles, and Claude has the default account and added account folders. A switch changes who pays for the next turn. It never moves a conversation. The desktop keeps a Codex limit reading for 60 seconds, and one reading per Claude account for 60 seconds.

Every fresh limit reading also goes into the table agent_usage_samples in the desktop database. The save runs on its own task, so a limit read never waits for it and never fails because of it.

RuleValue
Rowagent, account_key, window_kind, used_percent, resets_at, sampled_at. account_key is the Codex profile ID, or the Claude account ID.
Saved windowsOnly the windows for all models: session (the rolling five hours) and weekly. Codex windows are known by their length, 300 and 10,080 minutes. A Claude window for one model is not saved.
Repeat filterA reading is skipped when the newest saved one of the same window is under 10 minutes old, has the same reset time, and differs by less than 0.5 percentage points.
RetentionReadings stay 35 days. The desktop deletes older ones at most once an hour.
What a read returnsThe weekly readings of the active account from the last 14 days, and the time of the oldest saved reading of that agent.
Codex tokensFor Codex, the read adds up to 30 days of daily tokens and a summary from the app-server call account/usage/read. The answer is kept for 5 minutes. Claude has no token part.

Settings > Agents on the desktop shows both agents side by side, and then the usage, the history charts, and the accounts of the selected agent. It reads the history through the command agent_usage_history_command. The phones read the same data through system.agentUsageHistory.