Skip to content

Proposal: add explicit thread/unload app-server RPC #21889

Open

Description

@ZichaoLong

What variant of Codex are you using?

CLI / app-server

What feature would you like to see?

I would like app-server to expose an explicit thread/unload RPC.

Problem

thread/unsubscribe is currently a delayed unload path, not an immediate unload primitive. The
README says a thread remains loaded until it has had no subscribers and no activity for 30 minutes.

That is reasonable for reconnect-friendly clients, but app-server is also a multi-thread host:
one runtime may keep many unrelated threads loaded at once. In that model, clients need a narrow
way to unload one specific loaded thread immediately, without resetting the whole backend and
disturbing other loaded threads.

This is mainly about thread-scoped settings whose contract is "takes effect on next load". Today
the choices are:

  • wait for delayed idle cleanup
  • reset the entire backend to affect one thread

This is not a request to hot-update all memory behavior on an already loaded thread.
thread/memoryMode/set already covers persisted future memory-generation eligibility. The gap is a
per-thread unload boundary for settings whose effect is bound at thread load / session
construction.

Concrete use case

Suppose thread A is loaded but idle, and threads B/C are also loaded in the same app-server
runtime. A client updates some next-load-only setting for thread A and wants the next attach/resume
of A to pick it up immediately. Waiting 30 minutes is too slow, but resetting the backend would
also disrupt B/C. A narrow thread/unload would solve exactly that case.

Proposed API

A separate thread/unload RPC would keep the contracts clear:

  • thread/unsubscribe: subscription / reconnect semantics
  • thread/unload: explicit operator action

Suggested conservative first version:

  • method: thread/unload
  • params: threadId
  • response statuses: unloaded, notLoaded, active, otherSubscribers

Suggested behavior:

  • notLoaded: already not loaded; no-op
  • active: refuse unload while the thread is active
  • otherSubscribers: refuse unload if another connection is still subscribed
  • unloaded: shutdown completed and the thread transitioned to notLoaded

Suggested notifications:

  • reuse existing thread/closed
  • reuse existing thread/status/changed to notLoaded

Suggested call sequence

  1. Client persists a next-load-only setting for thread A.
  2. Client calls thread/unload(threadId=A).
  3. Server refuses if A is active or still has other subscribers.
  4. Otherwise server unloads A and emits the existing close/status notifications.
  5. The next attach/resume of A loads a fresh session and picks up the new setting.

Why not the alternatives

I do not think changing thread/unsubscribe to mean immediate unload is the right path, especially
since there is already adjacent upstream work around unsubscribe/unload timing and reconnect
behavior (#10954, #17398, #20487).

I also do not think config/batchWrite with reloadUserConfig: true replaces this need. Runtime
config reload is useful for refreshable user-layer settings, but it is not the same thing as a
per-thread unload boundary for next-load-only behavior.

I also do not think the internal unload_thread_without_subscribers(...) helper should be exposed
directly as-is, because it appears to be a background idle-cleanup path rather than a
request/response-quality explicit unload API.

Questions

  1. Would maintainers accept a separate thread/unload RPC?
  2. Should the first version be experimental-only?
  3. Is "idle + no other subscribers" the right initial guardrail?

Additional information

I reviewed upstream main on 2026-05-09 and did not find an existing public thread/unload RPC.
I also did not find an open or merged PR whose primary goal was to add that API, though I did find
adjacent thread lifecycle work in #10954, #14137, #14997, #17398, and #20487.

If this direction is interesting, I can provide a focused implementation plan or prepare a patch
after maintainer guidance.

Activity

  1. added
    enhancementNew feature or request
    CLIIssues related to the Codex CLI
    app-serverIssues involving app server protocol or interfaces
    on May 9, 2026
  2. Irvinwop commented on Sep 8, 2026

    @Irvinwop

    Adding a Codex Desktop use case for this request: please expose an explicit Release task runtime / Reload on next resume action in the task UI, backed by a task-scoped unload operation.

    While troubleshooting a custom provider configuration on macOS, correcting the endpoint in the user configuration did not update an already-loaded but idle task's cached provider. Returning to that task resumed the existing runtime. Restarting the entire app would also interrupt unrelated work, and the UI did not offer a way to close/release just that task.

    Navigating away is not an immediate release operation. In the installed Desktop environment, inactive tasks remained subscribed for roughly an hour; app-server's documented unsubscribe behavior can then retain an unsubscribed idle task for another 30 minutes. This is context from a customized local setup, not a claim of a newly reproduced stock-build regression.

    Suggested UX and safety requirements:

    • Release only the selected task's in-memory runtime and its own client subscription, preserving the task ID, transcript, notes, and resumability. Do not archive, delete, fork, or replay a turn.
    • Show why release is blocked: an active model turn, pending approval/tool execution, background terminal, or another client holding the task.
    • Default to refusing interruption; if an explicit force/stop-and-release option is offered, first show exactly which task-owned work would be stopped and require confirmation.
    • Return a bounded, observable result such as unloaded, already unloaded, or busy/held. Reopening should load a fresh runtime and its next-load configuration.
    • Leave every unrelated task and the app itself running.

    This would make recovery from stale per-task runtime state practical without requiring a whole-app restart or a long, opaque inactivity wait.

  3. nm-z commented on Oct 7, 2026

    @nm-z

    Another use case from codex-cli 0.160.1: a local agent manager runs many TUIs against the managed daemon (codex --remote unix://.../app-server-control.sock). To relaunch an idle thread with a different model, effort or service tier, it has to get the thread unloaded first. thread/unsubscribe only removes the calling connection, so once the original TUI has exited, nothing can unload the thread short of restarting the whole daemon, which interrupts every other thread's turn. An explicit thread/unload (refused while a turn is running) would let a manager swap one idle thread's settings without touching the rest.

  4. rmorse commented on Oct 15, 2026

    @rmorse

    Facing the same issue - using codex app server - and if I need to change UI, switch to the TUI, I can't because the session is locked, even though the previous subscriber process is completely terminated.

  5. cbusillo commented on Oct 16, 2026

    @cbusillo

    I hit this with codex-cli 0.163.0 while coordinating agent ownership. After a TUI quit and released its claim, a successor still saw the old thread as idle in the shared daemon. It reached notLoaded around the normal 30-minute expiry without a restart.

    A guarded unload would help, provided it refuses any attached client, running turn, pending approval/input or queued work. thread/unsubscribe from a new observer only affects that observer's connection. thread/read exposes no subscriber presence, so an external coordinator cannot safely distinguish an idle cached runtime from an idle client with unsent input.

    A read-only subscriber-presence field would also make that distinction observable. I traced this against the 0.163.0 lifecycle timer, caller-scoped unsubscribe, and Thread read schema.

  6. K0enna commented on Oct 18, 2026

    @K0enna

    Measured unsubscribe controls on the installed Windows backend

    I tested the unchanged Desktop-bundled codex-cli 0.163.0-alpha.10.2 from package 26.1013.4894.0 in isolated diagnostic homes, with one owned quiet stdio MCP process and no model generation turn.

    After this client's successful thread/unsubscribe:

    thread_unload_delay_secs in diagnostic home Context removal observed after unsubscribe
    1 1.125 s
    5 5.031 s
    Unset 1,800.016 s

    The subscribed controls retained their context and MCP process while observed. In every unsubscribe case, the MCP process exited before backend closure and the backend remained alive after cleanup. In the full default-delay case, context removal and MCP exit were both observed at 1,800.016 s; the backend subsequently exited normally, with no fallback termination or diagnostic survivors.

    These are observed timings from individual cases, not repeated latency statistics. The default-delay run independently confirms 30-minute delayed cleanup in this installed binary; a 60-second assumption from another version would have misclassified it. No authentication was copied into these homes; WebSocket warm-up returned 401, separate from the lifecycle checks.

    This supports the distinction in this proposal: thread/unsubscribe releases this client's subscription and allows delayed cleanup; it is not an immediate operator unload primitive. It does not prove cleanup under blocking-pool saturation, identify every Desktop helper's owner, or establish safe unloading when another client or background work still needs the thread. No live global unload-delay setting was changed.

    Related installed-backend and pipe-pressure evidence.

  7. Krarilotus commented on Oct 19, 2026

    @Krarilotus

    Data point from a multi-agent orchestration host using one shared codex app-server (codex-cli 0.163.0, Windows 11):

    • Loaded threads each retained a copy of the configured stdio MCP servers. In this particular setup, two configured servers and their helpers amounted to four processes and about 130 MB per loaded thread.
    • The host unsubscribed completed one-shot threads after its own 30-minute idle window. With the default Codex delay, thread/closed and MCP process exit followed another 30 minutes later.
    • At roughly 20 short delegated tasks/hour, one snapshot showed 26 loaded threads, 104 MCP-related processes and about 3.5 GB working set, mostly associated with already completed tasks. Those process counts and memory figures are specific to this MCP configuration.

    A model-free control used a stub stdio MCP server owning one child and a throwaway CODEX_HOME with server-wide thread_unload_delay_secs = 20: thread/start at +0.4 s, thread/unsubscribe at +1.4 s, then thread/closed and both MCP processes gone at about +23.4 s. Hard-killing a separate test app-server also ended its MCP tree. Teardown worked in these Windows tests; the practical problem was retention time.

    #55270 is merged and honours per-thread thread_unload_delay_secs, with 0 permitting immediate cleanup once there are no subscribers or activity. That appears to cover our retention-control need in versions containing the change; we have not tested that per-thread path on the observed 0.163.0 binary. The host would also need to unsubscribe promptly.

    An explicit guarded thread/unload, as proposed here, would still provide a clearer request/response contract for a host that knows an idle thread's task is finished, while preserving history and later resume.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app-serverIssues involving app server protocol or interfacesenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions