A gateway chat is a chat in some harness (Codex, Claude Code) whose inference the gateway serves. Each harness stores something about who served a chat. That stored identity decides which clients list the chat and which engine resumes it. This page is the contract for each harness, and the checklist for a new one.
Codex: gatefox by default, openai as an opt-in
Two ids for the gateway (1.8.0)
Since 1.8.0, new gateway chats are saved with the provider id gatefox. Chats saved
before 1.8.0 carry subscription_gateway, and that id keeps working: every engine the
gateway starts registers both ids, gateway desktop register publishes a row for each in
the native config.toml, the router lists chats under both, and the patched SDK
(gateway-executor-v4) treats both as the gateway. A chat is never rewritten by a resume.
| Id | Written by | Read by |
|---|---|---|
gatefox |
New gateway chats since 1.8.0 | 1.8.0 and later |
subscription_gateway |
Every release before 1.8.0; also a new chat on an engine an older release started | Every release |
The executor marker is GATEFOX_EXECUTOR since 1.8.0. Executors also get the older
HARNESS_GATEWAY_EXECUTOR, and the SDK reads either, so an SDK of either generation sees
the same state. gatefox chats migrate (see the rename plan) moves
saved chats from subscription_gateway to gatefox. Both ids stay registered, so a chat a
Mac has not migrated yet, or one another device still records with the old id, keeps opening.
Account names and saved chats
Gateway subscription account IDs, such as codex-primary and codex-secondary,
are separate from the harness provider identity described below. Renaming an
account must preserve its credential binding, every existing model alias, and
chatIdentity. Never rewrite saved chats or the native account configuration.
Keep existing picker aliases and their visibility unchanged when renaming account IDs. Running executors freeze their alias catalog, while the picker refreshes its catalog: exposing replacement aliases can make them selectable but unusable by loaded chats. Canonical duplicates may be added as hidden routes for new clients; keep every old and new alias routable indefinitely. Check the private alias reservation ledger for collisions and migrate only exact matching route identities, including retired aliases. Move the automatic-model-update scope with the ID.
Live configuration deliberately refuses provider-ID rebinding. Activation requires
a fresh inference process. In a managed service, the inference child's verified
SIGUSR2 handler restarts only when idle; do not restart the supervisor or chat
executors to change names. A busy child leaves activation pending.
scripts/e2e-codex-account-names.mjs checks loaded-chat continuation, cold resume,
account separation, and model switching with the real signed SDK and mock inference.
Its adapter scenario reproduces a refreshed picker advertising an alias absent
from a loaded executor, then verifies switching through the stable picker aliases.
Releases 0.4.7 to 1.7.x record new gateway chats with the provider id
subscription_gateway; 1.8.0 records them as gatefox. A Mac can opt in to the native
identity, openai, with "chatIdentity": "openai" in its gateway configuration.
The phone is why the choice matters. The ChatGPT app's Codex list, and a stock Codex
engine's thread/list without a provider filter, show only chats saved with the engine's
own provider, openai. With the native relay in the path the phone does not depend on
that: the relay sends its requests through the adapter, the adapter lists the gateway's
chats beside the native ones (see chat lists), and continuing a gateway
chat goes to the gateway. No identity switch is needed then, and continuing is safe.
When to opt in. Only on a Mac whose native relay is verified: gateway native status
shows it connected, and a gateway chat was listed and continued from the phone through it.
Opting in only adds that a stock engine, such as the phone's when the relay is down, also
lists the new chats. That is also the risk: without the relay, the phone continues a chat
on the app's own engine, which uploads the chat's history to OpenAI under your account. A
chat on a gateway alias is then refused, since OpenAI has no such model; a sub-agent on a
native model, such as gpt-6.1-sol, runs on your own ChatGPT account. Leave the default on
any Mac whose relay is not verified.
| Where | subscription_gateway (default) |
openai (opt-in) |
|---|---|---|
Rollout session_meta.model_provider and state_5.sqlite threads.model_provider |
subscription_gateway |
openai |
| Model field | the gateway alias, such as codex/gpt-6.1-sol |
the same alias |
| Sub-agent of the chat | the parent's id, model often a native slug | openai, model often a native slug |
| How the adapter recognizes it | the provider id | the registry, then the ancestors, then the alias |
| Listed by a stock engine with no filter | no | yes |
| Listed through the adapter with no filter, or with only the native default | yes (0.4.7 adds it to the list) | yes |
Existing chats are never rewritten, whatever the setting. A resume names the provider id
the chat was saved with, because the SDK replaces a chat's SQLite provider when a resume
names another one. A fork is a new chat and gets the identity of the connection that
makes it. Turning the opt-in off again (remove the key, or set "legacy", and reconnect
the desktop) keeps every openai chat working: every patched executor serves both ids.
Releases before 0.4.7 ignore the key.
Codex 0.166 refuses to resume a chat, or to take input on it, when its saved provider
differs from the executor's configured one ("This chat uses subscription_gateway, but
your current provider is openai"). Inside a marked gateway executor the executor patch
treats the legacy id and the loopback openai alias as one provider in both checks
(same_gateway_executor_provider), so a legacy chat still resumes under the id it was
saved with. Every other pair, and every unmarked engine, keeps the SDK's strict check.
How an executor serves openai
A gateway executor is the pinned gateway SDK (gateway-sdk/gateway-codex) started by the
adapter or by the shared execution host. It is not the built-in OpenAI provider. All of
this ships and runs in 0.4.7 whatever the setting, so an opted-in Mac and one that
opted out again serve the same chats.
- The adapter starts every executor with
GATEFOX_EXECUTOR=1and the olderHARNESS_GATEWAY_EXECUTOR=1in its environment. Native engines never get it: the adapter and the shared native host remove an inherited marker (either spelling) before starting one. Each executor sets both to0for the commands, hooks and shells it starts (shell_environment_policy.set), so nothing a chat runs counts as an executor. - The executor's launch settings register the gateway's own provider under both ids,
model_providers.gatefoxandmodel_providers.subscription_gateway(loopback base URL, thegateway tokencommand,supports_websockets = false,request_max_retries = 2,stream_idle_timeout_ms = 90000), and setmodel_providerto the identity for new chats. - The gateway SDK patch (
scripts/patches/gateway-executor.patch, capability retained in capabilitygateway-executor-v3) makesopenaia second name for that provider, but only when the marker is1and the provider's base URL is plain HTTP on a loopback address with no credentials in it (a name such as127.0.0.1.nip.ioor a mapped IPv6 address does not count). Inference, authentication, retries and timeouts are therefore exactly the legacy provider's. The SDK keeps no ChatGPT or API-key credential, so nothing it sends elsewhere carries the gateway token. - Because the alias is not named
OpenAI, the SDK compacts locally (it never sendscompaction_triggeritems, which the gateway refuses), and does not send WebSocket requests or internal metadata. The patch also keeps context-limit compaction on the selected model inside the user's turn for both ids. - Every executor sets
analytics.enabled=falseandopenai_base_urlto the gateway. If an executor ever runs without the patch,openaistill reaches only the loopback gateway, which refuses the request; it never reaches OpenAI.
The adapter uses openai for new chats only when the Mac opted in and the package's
gateway SDK declares gateway-executor-v4. Otherwise new chats are gatefox; a stock
Codex executor (a checkout without the artifact) serves it too, because both ids are
registered rows. A new chat on an engine this connection did not launch (a shared host's,
found by its chat) keeps subscription_gateway, since that engine may belong to an older
release that registers only the old id, and an executor that records a new chat with any provider
but the requested one is refused.
An already-open adapter keeps the identity it selected for new chats. If its original
managed package is pruned after an install, it may recover only to the verified SDK in
that installation's current release. A verified replacement can serve an existing
legacy connection without changing it to openai; an existing openai connection
remains fail-closed unless that replacement proves gateway-executor-v4. Saved chats
always retain their saved provider id.
The provider row in native configuration
Every engine the gateway starts registers gatefox and subscription_gateway with -c settings.
The ChatGPT app's own engine reads only the native config.toml. ChatGPT is a login item
and can start its engine before the gateway service sets CODEX_CLI_PATH. Without a row
in config.toml, every saved gateway chat then fails to open with "Model provider
gatefox not found" (or subscription_gateway for an older chat), whatever model the chat used.
gateway desktop register therefore publishes [model_providers.gatefox] and [model_providers.subscription_gateway]
in the native config.toml, after it sets CODEX_CLI_PATH. It runs at installation and
at every service start, so the row follows a changed port or configuration path. The row
is the same table as the adapter's -c settings (src/native-provider-row.ts): the
loopback gateway and the gateway token helper. The gateway writes it through the app's
own engine, as API mode writes its settings, so comments and every other key stay as
they are.
- What registration writes: the row only. It never writes
model_provider, a model or a catalog, so new native chats keepopenai. - When it skips: a row it already wrote is left unchanged. A row with the same id that the gateway did not write is never replaced.
- Failures: an unavailable engine is reported (
native_provider_rowtelemetry and thenativeProviderresult field) and never fails registration. - Uninstall:
gateway uninstallremoves the row only when the gateway wrote it. - Client edits: the adapter still refuses a client edit that saves the row; only registration maintains it.
A chat opened by a stock engine this way gets its replies from the gateway, but without the adapter's extras, such as gateway tool registration and the compaction patch. The next app launch through the wrapper restores the full gateway path.
Routing follows the chat
The adapter decides a chat's engine from the chat, never from the provider id. The
classifier (src/gateway-chat-classifier.ts) answers with the first rule that matches:
legacy_provider: the chat is saved asgatefoxorsubscription_gateway.registry: the chat is in<state>/gateway-chats/<scope>.json.parent: a sub-agent whose ancestor (up to 8 levels) is registered, saved asgatefoxorsubscription_gateway, or on a gateway alias.alias: the chat's model is a gateway alias and not a native slug.native: anything else.
Before it classifies a chat, the adapter reads each of its ancestors that is neither in
the registry nor already seen on this connection (thread/read without turns, up to 8
levels, each once per connection). So a sub-agent on a native model is found even when its
parent's registry entry was lost, or when it was spawned two levels deep while no desktop
was connected.
The registry is written before a new chat's reply, before a sub-agent's announcement is
passed on, and, since 0.4.7, when a chat that only its ancestors or its alias identified is
routed to an executor. Throwaway chats (composer predictions) are never written. A failed
write is reported once as gateway_registry_write_failed. Such a chat is still found: a
gatefox or subscription_gateway chat by its id, an openai chat on a gateway alias by its alias,
and an openai sub-agent on a native model only through its ancestors.
Chat lists
When a thread/list asks only for the native engine's default provider, by naming it
alone or by naming none (and no parent or ancestor filter), the adapter asks the engine
for that provider, gatefox and subscription_gateway. So a client that lists the native
identity through the adapter, such as the phone through the relay, also sees the gateway's
own chats, old and new. An empty list (every provider), several providers, another
provider, or a parent filter is sent unchanged. The native default is read from the
engine's configuration once per connection, and again after a configuration write.
Runtime state on mobile lists
The native engine remains the source of list membership, ordering, cursors, titles,
saved metadata, and turns. After it returns a page, the adapter considers only saved
(notLoaded) row ids, intersects them with a bounded gateway loaded-id read, and may
replace only those rows' runtime fields: status, model, reasoning effort,
environments, and direct-input capability. A native row already live always wins. One
1.9-second list budget covers discovery and metadata reads; a host failure, timeout, or
unavailable gateway SDK leaves the native page unchanged.
For a shared generation that no normal adapter connection already observes, the
adapter opens a passive SDK connection solely to receive real
thread/status/changed notifications. It never resumes a chat, answers approvals,
forwards content, or owns a gateway engine. SDK new-thread subscriptions are released
on that passive connection. Discovery runs about every five seconds after a client has
listed chats, so it can miss transitions that happened before an observer attached;
notifications do not replay state or history. The SDK has no unread field: clients own
their unread bookkeeping.
Native chats and per-connection gateway chats on separate desktop and relay engines use a private status broker in each adapter. It exposes only loaded chat IDs, runtime status, model, effort, and direct-input capability through a user-owned Unix socket. Registrations are scoped to the native home and checked against their process and socket identities. The list client receives genuine native status events and enriches saved rows without resuming chats or taking over an engine. An unavailable owner contributes no runtime state. Existing native adapters start publishing after their normal upgrade or reconnect; the broker cannot observe an older adapter that lacks it.
The native and gateway loaded IDs are combined before applying loaded-list
pagination. Ordinary chat-list membership and pagination still come from the
native engine. The checks in scripts/e2e-chat-state.mjs, including --native and
--private,
use two clients and the real SDK with a local mock provider. They establish
running-state hydration and idle-event delivery, without paid inference. They do
not establish the phone's unread rendering.
What the phone does
Phone continuation through the relay is implemented. On 23 Oct 2026 the isolated
scripts/e2e-native-relay.mjs --expect-unfiltered-gateway-listing check passed with
the real signed 0.163.0-alpha.11.1 app engine: it listed native and gateway chats,
ran both, and continued the gateway chat with its history on a second stream.
It also confirmed that a private engine refuses a second writer while the app
holds a chat, then permits continuation after the app releases it. This tests the
relay protocol with a mock phone and mock inference, not the phone's rendered UI.
| Without the relay | With the relay | |
|---|---|---|
| List, default setting | The app's own engine answers: no gateway chat is listed | The adapter answers: every gateway chat is listed |
| List, opted in | New openai gateway chats are listed, older ones are not |
Every gateway chat is listed |
| Open | History is shown | History is shown |
Continue an openai chat on a gateway alias |
The app's engine uploads its history to OpenAI, which refuses the unknown model | Routed to the gateway executor |
Continue an openai sub-agent on a native model |
The app's engine uploads its history and runs it on your ChatGPT account | Routed to the gateway executor |
Continue a gatefox or subscription_gateway chat |
Not reachable from the list | Routed to the gateway executor |
Rollback
Only a Mac that opted in has openai gateway chats. Rolling it back:
| Back to | Top-level openai gateway chats |
openai sub-agents |
subscription_gateway chats |
|---|---|---|---|
| 0.4.4, 0.4.5 or 0.4.6 | Listed and continued: the registry routes them to the gateway. Those releases resume a gateway chat with subscription_gateway, so each one resumed there is saved as subscription_gateway in SQLite (the rollout keeps openai) and drops off a stock engine's list |
Their executor runs patch v1, so when a parent resumes an openai child it uses the built-in OpenAI provider (core/src/agent/control/spawn.rs keeps the child's saved provider), for which an executor has no credentials; such children do not continue on the gateway |
Unchanged |
| 0.4.3 or older | Listed, but they cannot be continued: those releases classify by provider id and send the alias to OpenAI | Not continued on the gateway | Unchanged |
Claude Code: no provider id
Claude Code sessions store the model name, never a provider. There is nothing to
rename, and no list filters by provider. A gateway session is one whose model is a
gateway alias (see Messages). Remote Control needs api.anthropic.com
and a claude.ai sign-in, so no naming change makes a gateway session reachable from the
Claude app.
Copilot
Copilot models reach Codex as gateway aliases, so a Copilot chat in Codex follows the Codex contract above. The Copilot CLI's own sessions are the gateway's private state (live Copilot sessions) and are not listed by any harness.
Checklist for a new harness
- Find what the harness stores per chat that names who served it (provider id, base URL, account, model), and which clients filter their lists by it.
- Prefer the harness's own native identity for new chats, so its stock clients list them. Never rewrite chats that already exist; a resume must name what the chat was saved with.
- Keep routing independent of that identity: record each new chat, and its sub-agents, before the client sees it, and classify by chat, not by the stored id.
- Make sure the native identity cannot send a gateway chat to the vendor: mark the gateway's executors, check that the endpoint is loopback, and fail closed without them.
- Turn off the harness's analytics and any account-backed calls in executors, and check that no request outside the gateway carries the gateway token.
- Keep the harness's compaction and retry behavior the same as under the gateway's own provider, and test it with the real harness binary against a mock upstream.
- Put the switch behind a per-Mac setting, default it to the old identity until the client that reaches the chats (for Codex, the native relay) is verified, and write down what an older release does with the new chats before shipping.
The end-to-end check for Codex is scripts/e2e-identity.mjs.