DOCS

Chat and executor lifecycle

Sub-agent hydration, executor re-attach, shared acquires, chat notification order and queued messages for gateway chats.

About 10 min read · 5 sections

On this page

Part of the Codex app-server adapter.

Opening an unloaded subagent

Multi-Agent V2 children resume through their loaded immediate parent's SDK. If a model catalog or role change created a newer executor generation, the adapter must still open an unloaded child on the parent's existing generation. Starting a current-generation executor instead can return -32600: “cannot resume an unloaded multi-agent v2 sub-agent through its parent; resume the parent first, or use thread/read to inspect it”. The desktop then fails hydration even though the child's saved history remains readable. This affects local and API models alike.

The adapter checks the saved child's identity and gateway classification, then uses the immediate parent's existing binding or shared owner for thread/resume. It does not load a parent, start a turn or change caller settings. If no parent is loaded, the SDK's existing refusal remains; thread/read and thread/turns/list can still inspect saved history. Paginated histories require the native thread_history_1.sqlite store as well as the rollout files when reproducing a read in an isolated home.

Many requests for unloaded chats at once

A reconnecting app sends one request per chat in its sidebar, such as thread/attachment/list, and an unloaded gateway chat needs a gateway executor to answer. Since 1.6.5 requests for the same generation on the same host connection share a single execution/acquire, whose frame carries the whole catalog; the host's lease covers all of them. Each request reads the current settings first, so one with different settings asks for its own generation. A failed acquire fails the requests waiting on it and is not remembered; the next request tries again.

The adapter also keeps its answer to "which gateway SDK" while the SDK's files are unchanged (device, inode, size, times, owner and mode), instead of hashing the file on every request. That answer only names the SDK. Whatever spawns one still hashes the bytes first and refuses a mismatch: the execution host on each spawn, and the adapter for its private executors.

Re-attaching a gateway chat

A gateway chat runs on an executor that can go away under a loaded chat: the shared host restarts (a service upgrade or restart), a private executor exits, or the host drops this adapter's connection. Since 0.4.7 the chat's next request does not fail for that. The adapter first loads the chat again: it finds the host again (a kick and up to 20 seconds of waiting, taken once however many chats wait), takes the generation that still owns the chat or the current one, and sends thread/resume the way a desktop resume does, then the request itself. The resume carries only what the SDK cannot restore from the chat's own history (config, instructions, personality and the speed tier), always with excludeTurns: true, and never a model, provider id, approval policy, approvals reviewer, permissions, folder or workspace roots: the SDK applies the latest values saved with the chat to a resume that omits them (an explicit folder wins over the saved one, and explicit roots skip their restore), so a chat that tightened its approvals, changed its model and effort or moved to another folder after loading keeps those changes through a re-attach. A chat this connection did not load, such as a sub-agent, one it has forgotten, or one whose parameters it evicted (a connection keeps the most recent 512), is resumed with its saved settings alone; the replayed config drops the keys that would block that restore.

Each re-attach is logged as executor_reattached or executor_reattach_failed (diagnostics).

When only a shared executor client's socket closes during an active gateway turn, the adapter attempts that same re-attach immediately. It reads the owning SDK's saved turn state before updating the desktop: an in-progress turn is rejoined and a terminal turn is forwarded once. It never repeats a prompt, steering request, tool result or approval decision. Genuine pending approval requests retain their frontend IDs and require an explicit answer. Paginated histories restore completed display items only when the SDK recorded their completion time. If an active turn's earlier item history is unavailable, the adapter keeps its genuine future stream; reopening the chat may still be needed to reload earlier output. A turn already finished during the disconnect requires readable item history before the adapter reports its recovered completion.

A listener that outlives its release

An adapter can stay open after the installer prunes the release directory it started from. When its complete original package directory is gone, it may load the SDK from the managed installation's current release and continue saved chats without reconnecting. That replacement is accepted only when the installation marker, current link, selected release marker, package version, SDK receipt, component hashes, capability and pinned SDK source all verify. The link must select a direct, user-owned release beneath that installation; a checkout, escaped link, partial old package, or present package whose SDK is missing or invalid never falls back.

The connection keeps the provider it selected for new chats. A gatefox connection can use a newly verified SDK without changing its identity. A connection that selected openai continues only when the replacement proves the gateway executor capability; it never resumes gateway history on a stock executor.

An already-loaded executor retains the catalog it loaded. Changing a catalog file cannot alter that generation, and a live owner continues serving its loaded chats. After an update, let the shared service adopt the release when it is quiet; do not force-kill it. To move one affected chat sooner, unsubscribe it from every frontend, wait for its thread/closed event (or for the host to report no owner), then resume it through an adapter on the fixed release. A resume before the old owner unloads returns to that owner.

Empty chats and message order

The SDK answers a thread/start or thread/fork request before it announces the new thread. The desktop depends on that order: it hides a prewarmed chat (the empty chat behind an unsent draft) only when the reply arrives before thread/started. The adapter records a gateway chat's tier before it answers, so it holds the new thread's notifications until the reply is written and then releases them in SDK order. Chats that are already known keep streaming while a start is pending, a failed start still releases the held notifications, and a buffer of more than 512 held messages is written out in order rather than growing. A server request (an approval or elicitation, which carries its own id and never waits) still follows every message already held for its own chat: those held notifications are written first, so the desktop is never asked to answer an event it has not seen. Before this, a gateway prewarm showed up in the sidebar as an empty chat, and opening it failed with "Failed to resume chat".

A request for a chat that no SDK knows (no rollout, nothing loaded) goes to the native SDK unchanged, so the desktop receives the SDK's own answer: an empty attachment list, a notLoaded unsubscribe, or "no rollout found for thread id", which the desktop recognizes when it archives or deletes such a chat. Any other failure of the provider lookup still fails closed.

Queued messages (Cmd+Enter)

thread/queue/* goes to the chat's own engine like any other chat request, and the SDK owns the queue: it is saved on disk in the shared queue database, the next message starts when the running turn completes, and an interrupt pauses the queue. With gateway SDK capability gateway-executor-v3, a failed gateway turn also pauses automatic queue dispatch. This applies to every provider routed through that executor, including Claude, Codex, Copilot, and ordinary API models. Queued messages keep their IDs, contents, and order in the native database. Adding or editing a queued message, another connection writing to the queue, or resuming the chat after a restart does not release the pause.

An explicit new turn can continue the failed work from saved history. After it succeeds, queued messages run in their existing order. An explicit thread/queue/start can instead start the selected queued message. The failed turn remains a real failure; the gateway does not fabricate an interruption, retry a subscription limit, or choose another account or model. Unmarked native Codex engines keep their own queue behavior. Clients outside this executor, such as Claude Code or Claude Desktop, own their own queues.

The automatic queue start checks the pause while reserving an idle turn, so a concurrent failure cannot slip past a prior queue inspection. Older SDK artifacts retain their previous behavior; the release must build the v3 SDK before this protection is available.

The SDK reads every attached local image or audio file while it enqueues a message. macOS gives file access to the desktop app, not to the service that owns shared executors, so a screenshot from a protected folder failed there with -32600 "local queued attachment is invalid: Operation not permitted". For thread/queue/add and thread/queue/update, the adapter (which runs under the app) now copies each attached local file into a private queued-attachments directory under the gateway state directory, sends the SDK the copy, and removes the copy when the call returns; copies older than ten minutes left by a crash are removed. The copy keeps the file extension, because the SDK takes the audio type from it, and is cut one byte past the SDK's own size limit so oversized files still get the SDK's size error. A file the adapter cannot read is sent unchanged, so the SDK reports the same error as before. Executors that run under the app (per-connection engines) are not changed. Paths and file contents are never logged.