Laptop sleep and offline periods are expected. Recovery must not depend on changing power settings, keeping a desktop window open, or moving a workspace.
Client loses its connection
→ its transport detaches
→ the execution machine keeps its owned SDK and turn
→ a fresh client reads and joins that existing turn
The execution machine must itself be running to perform work. Sleep suspends
its processes; a shutdown ends them. On reconnect, the saved chat remains the
source of truth. A missing connection alone is not proof that a command failed,
and the adapter must not resend turn/start or repeat a tool command to make
the interface look active.
When a shared gateway executor loses only an adapter socket during an active turn, the adapter reattaches the chat and verifies the SDK's saved turn state. It never accepts a pending approval or repeats a caller action; unavailable or unverified state remains available through the ordinary explicit resume path.
Discover running chats after restarting the app
A fresh desktop connection discovers recent chats through thread/list. The
adapter overlays the owning shared executor's current status on saved native
notLoaded rows. An active status lets the desktop show its running indicator
without starting or resuming a turn.
If the first list misses the executor or its metadata deadline, the adapter
retries the listed candidates in the background. Once the engine answers, a
verified thread/status/changed snapshot updates the running indicator without
requiring another manual list refresh. Missing or unavailable engines do not
prove a chat idle. Candidate counts, reads, and deadlines are bounded; real SDK
status events take precedence over earlier metadata reads.
The adapter remembers up to 512 listed candidates and rotates through four at a
time on its five-second refresh. Successful snapshots can arrive while a slow
neighbour is still pending, and an unsuccessful delivery remains eligible for
retry. Large lists can therefore take several refreshes to recover fully.
The indicator and an attached transcript are separate. Opening a chat uses the
normal resume path to join its stream. The gateway does not replay turn/start,
commands, approvals, or completion notifications to make it look connected.
This recovery covers rows the client has actually listed; a chat outside that
list, a host with auto-connect disabled, or the desktop's catalog-history mode
needs its own discovery path. It does not turn API-mode app-owned engines into
durable service-owned executors.
Verify cold and delayed discovery with the isolated SDK fixture:
node scripts/e2e-chat-state.mjs --package /absolute/verified/package \
--root /tmp/task-cold-reconnect --restart-discovery
node scripts/e2e-chat-state.mjs --package /absolute/verified/package \
--root /tmp/task-delayed-reconnect --restart-discovery --delayed-discovery
The previous application client disconnects during synthetic inference. The new client starts with an ordinary state-database list; it must find the same running turn and later its real completion without starting inference again. The delayed case makes the first executor lookup unavailable and requires automatic status recovery without a second list. No account or paid inference is used.
Native and gateway ownership
Shared gateway executors already belong to the service. Shared native execution uses the app's original, signature-verified SDK and its existing native home. Disconnecting a client of either shared executor closes only that client's socket. A private executor belongs to its adapter and ends when that adapter ends, so native sharing must be active before a client disconnect can preserve that executor.
The native SDK must pass the real shared-native checks before its version is accepted. Those checks cover every frontend disappearing during inference and during a pending user approval. Reconnect must preserve the SDK process and turn identity, recover saved results, and never answer an approval automatically. See native connected machines for verified versions and verification for the fixture.
Managed private native launches whose configuration and home match an existing native relay service plan reserve that home before spawning the SDK. Private engines may share that reservation with compatible private engines, but a shared host cannot claim the home until those private writers have ended. A live or uncertain owner is never displaced.
Adopt sharing without closing active chats
Use gateway service shared-native --on-restart to stage native sharing for
the existing managed service. The command records intent separately from the
active service plan. It preserves gateway sharing, tunnels, components,
authentication, native configuration, and running connections.
Only a subsequent primary LaunchAgent daemon start may apply that intent. Startup verifies the current plan, SDK, and writer census again. A changed plan, unsupported SDK, surviving old adapter, or unknown writer leaves the intent pending and keeps the current valid service plan authoritative. Startup changes only the native sharing flag and saves the original plan first. A manual daemon invocation cannot activate the intent.
Staged, installed, and active are separate states. Staging is not evidence that
an existing private executor has become durable. After natural adoption, check
gateway service status. For combined native and gateway sharing, also require
gateway rollout --verify; its direct-tool canary requires gateway sharing.
Recovery boundaries
Explicit Stop, user-paused or blocked goals, approval requests, authentication and billing failures, and usage limits retain their meaning. Shared ownership does not clear them or create another inference turn.
The SDK handles provider stream reconnection. A request whose retries are exhausted remains a real failure, and queued work remains available for an explicit continuation. The gateway never replays uncertain side effects.
Unread dot after sleep
For Gateway-owned reporting that does not depend on receiving a live completion in the Codex window, use Activity. It distinguishes sampled SDK state from recorded outcomes and unknown execution. It does not change Codex's unread state or automatically continue failed work.
The desktop renderer sets its unread dot and completion alert only when it receives a live terminal event for a loaded thread. After sleep, it resumes the thread but can miss a terminal that the shared executor emitted while no app session was attached; its durable SDK read receipt does not repair that state.
The adapter keeps a private host-wide ledger at
<state>/completion-replay.json. It records a followed turn separately for
each device and marks it delivered when that device's adapter forwards its
terminal frame. The ledger is retained for a future explicit client integration
whose receipt and read semantics make replay safe. Stock adapters deliberately
do not read it after thread/resume, query the SDK for an old turn, or
synthesize a historical terminal. The stock renderer can apply such a terminal
after a foreground read and mark the chat unread again; a resume does not prove
focus or read intent. The record is shared by every local adapter process and
is bounded to 512 device/thread entries and seven days.
device follows turn → app disconnects → SDK finishes → that device resumes
↓
SDK receipts and history remain authoritative
The ledger's bounded claim machinery remains available only to that future
explicit integration. A normally connected app still receives its own live
terminal event. Offline completion does not currently guarantee a stock
renderer unread dot or alert, and the adapter never infers or changes read
state from thread/resume.
An SSH proxy identifies its client by hashing its SSH client address with a
local, random private salt, so the ledger receives only a stable local pseudonym
instead of an address. It carries that pseudonym only in the private Unix
WebSocket upgrade's X-Harness-Gateway-Device header; after the upgrade the
listener validates one 32-hex value and synthesizes the internal
gateway/client notification before decoded client frames. This leaves the
native HTTP/WebSocket byte pipe intact. Sessions without that header are local; relay
registration identifies only the host's remote-control route, not where an
inbound app session originated. The pseudonym never establishes focus or read
intent, and stock adapters do not use it for resume replay. If a future explicit
client integration uses the ledger, an SSH address change would be a distinct
device identity under that integration's contract.
See the isolated unread prototype for the renderer evidence and the separate client-side testing boundary.
Mosh's terminal reconnection does not provide these JSON-RPC ownership or approval guarantees. The existing SSH and connected-machine transports remain in place; the executor lifetime is independent of those connections.