Repository navigation
fix(acp): disable codex background session MCP fleet via CODEX_CONFIG - #20296
Conversation
UX Review (Opus 5.5) — ⏭️ skippedRevision |
First Principles Review (Opus 5.5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks. The one open premise is that codex-acp merges Notes
What this change shipsInventory (5 items) — 3 justifiedIntent: stop Codex background helpers (titles, navigation, suggestions) from piling up MCP processes until the gateway runs out of memory. FIX.
[FIRST-PRINCIPLES-REVIEWED] 48b1954 Description read[DESCRIPTION-READ] af051d4beba0d5698ab719fd78d274114ae2fc58047b8840ba547115fc46480e That sha256 names the PR description this verdict read. Recompute it with |
Design Review (Opus 5.5) — ✅ PASSDesign-level review of Design-Verdict: PASS No design problem blocks this: a spawn-time Notes
[DESIGN-REVIEWED] 48b1954 Description read[DESCRIPTION-READ] e8371e59fe7a283a43b37117f0800c33ffa0ce4b2f0dfb87a44b215b76d68310 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
GPT 6.1 Review — ✅ no blocking findingsGPT 6.1 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 5.5 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
5cc97fe to
ccce4c4
Compare
91c97f1 to
b067026
Compare
7314a6d to
f1a7d85
Compare
|
Live evidence for the open question in the Design and First Principles reviews, measured with codex-acp main (2.2.2, bundled Codex 0.160.1) against a local fake model provider and 2 recording MCP servers in a throwaway On This PR does not reach codex-acp's title threads. |
a8a6ee6 to
807250a
Compare
|
GPT F1 (blocking, |
|
Design Review BLOCK (provider behaviour unverified), status on head 807250a: still open. It needs a live codex-acp 1.11.0 run, which the code cannot provide. This host has no codex-acp install, so the 5-step check in the description has to be run by the owner on a machine with Codex configured. The Watch item "skip is silent" is addressed on this head: when |
|
Hold at 807250a until the provider premise is verified. The code does what it says: a background Codex spawn reads the What is unresolved: the Design Review blocker, which the author's latest reply says is still open. If codex-acp ignores What would clear it: the Manual verification steps run on codex-acp 1.11.x (or a stated minimum version that is known to work), with before/after MCP child counts and title generation still working. Non-blocking notes: the leak still reaches sub-agent sessions, pooled reuse and the entitlement probe, and title threads stay covered only upstream; naming those in Not a goal would help. Background sessions losing the operator's MCP servers is better written as a — crew-pr-reviewer (r138) |
bolichen97
left a comment
There was a problem hiding this comment.
[operator: bolichen97] Requesting changes for one breaking case on the codex background runtime.
Stale server names make every background session fail (src/kiro_crew/acp/harness/codex.py, the _read_codex_mcp_server_names call in resolve_spawn and the CODEX_CONFIG injection in apply_spawn_env).
The server names are read once, when the background runtime spawns, and frozen into CODEX_CONFIG. codex re-reads config.toml on every session/new, and the runtime can live for hours. If the user removes or renames an [mcp_servers.<name>] entry in that window, the frozen {"<name>": {"enabled": false}} no longer merges onto a real entry. It becomes a server entry with neither command nor url. codex's TryFrom<RawMcpServerConfig> rejects that with invalid transport (codex-rs/config/src/mcp_types.rs), so the config fails to load. Every background session/new (titles, suggestions, navigation) then fails until the runtime recycles. On main the same edit is harmless.
A related case: the names come from ctx.environ's CODEX_HOME, while agent.env is merged later and can point the child at a different CODEX_HOME, which yields the same orphan entries.
Possible fixes: use a global "skip MCP startup" switch if codex or codex-acp has one; or recycle the background runtime when config.toml changes; or prove (with a test or a live run) that codex tolerates an override for a missing server. Please add a test for the "server removed after spawn" case either way.
Non-blocking:
- When
CODEX_CONFIGis already set, the fix is skipped with only a warning. JSON-merging the disable map into the existing value would keep it working. - codex-acp replaces
mcp_serverswhensession/newcarriesmcpServers. If the background agent ever mounts one, the override is lost. Worth a comment or test. import tomllibcan move to module scope.- The PR body's manual before/after process count is still pending; a live run would confirm
enabled: falseviaCODEX_CONFIGactually stops the fleet.
The rest looks right: the override deep-merges over config.toml without touching the user's file or auth, it is scoped to the background agent only, and an existing CODEX_CONFIG is preserved.
AI-assisted, approved by bolichen97
Background (kirocrew-lite) codex runtimes read the mcp_servers names from CODEX_HOME/config.toml off the event loop and inject a CODEX_CONFIG override with enabled:false per server, so high-churn background sessions stop starting an MCP fleet that session/close never reaps. An existing CODEX_CONFIG wins and the skip is logged. Project-level .codex/config.toml is a known residual. Fixes #13324
807250a to
56f5f36
Compare
|
@bolichen97 thanks, the stale-name case is real. I checked it against the adapter: Fixed on head 48b1954 (rebased on main, second commit; re-pushed once because CI did not start on 56f5f36):
Why not the other fixes: codex has no global "skip MCP startup" switch (no such key in the config schema), and recycling the background runtime on a Residuals, noted in the code comment: a server renamed after spawn starts its fleet under the new name until the runtime recycles (same as main). A server that switches between Not changed: merging into an existing |
codex re-reads config.toml on every thread/start while CODEX_CONFIG is fixed for the adapter's lifetime. A server removed or renamed after spawn left an override entry with only enabled=false, which codex rejects as "invalid transport", failing every background session/new. Each override entry now also carries an inert placeholder for the transport key the server had at spawn (command or url), so an orphaned entry still parses as a disabled server. Adds a test for the server-removed-after-spawn case and moves the tomllib import to module scope.
56f5f36 to
48b1954
Compare
|
HOLD at @bolichen97's change request at 807250a (stale server names make every background session fail) is still active. The author's reply describes a fix on this head: each disable entry carries an inert The live check from our r138 hold is still open. Both current-head lanes name it as the evidence for two unanswered notes: that the placeholders really parse, and that a background What would clear it:
The rest is sound: the override is limited to the background agent, an existing — crew-pr-reviewer (r146) |
bolichen97
left a comment
There was a problem hiding this comment.
Approving 48b1954. I read the full diff at this head.
- A background codex runtime now gets a
CODEX_CONFIGoverride with oneenabled = falseentry permcp_serversserver inconfig.toml, so it does not start the user's whole MCP fleet. - Each entry keeps a placeholder for the transport key the server had (
commandorurl), so codex still parses it if the server is removed or renamed after the spawn. A server with neither key is left out, as codex already rejects it. config.tomlis read with stdlibtomllib; a missing, unreadable or malformed file yields no override, so nothing changes for that spawn.- The new tests cover the override and its edge cases.
No regex, glob or case-blind matching is added. All 97 checks pass, and the Design, First Principles, Opus and GPT lanes pass on this head.
Problem / Motivation
Goal: Stop Codex background sessions (title gen, navigation, folders, suggestions) from accumulating MCP server processes that survive after the session closes.
Codex starts a per-session MCP fleet (~44 processes) from
mcp_serversentries inCODEX_HOME/config.tomlfor everysession/new.session/closeevicts the ACP session from codex-acp's internal map but does not terminate those MCP children. Background sessions (kirocrew-lite) are high-churn — many per conversation at a rate the user never controls — so the fleet grows without bound until the background runtime hits its 6-hour age ceiling andkillpgreclaims the whole group.Reporter hugoncosta (
codex-acp1.11.0) observed ~855 processes / 21 GiB after 4 hours of a recurring dashboard session, cleared only by restart.Why it matters
Any operator who selects the Codex backend and has MCP servers configured in
~/.codex/config.tomlwill hit this in a long-running dashboard session. The accumulation causes OOM, which kills the gateway and interrupts all active sessions.Not a goal
AcpRuntime): durability for the reconciler after a gateway OOM restart.AcpClient._spawnwithKIROCREW_SPAWN_INSTANCE/KIROCREW_SPAWN_HOME.CODEX_HOMEaccess unchanged..codex/config.tomloverrides: names are read once from the user-levelCODEX_HOME/config.tomlat spawn time; servers added by a project config are a known residual (see code comment).Intended? / Repro
Intended? No. On main,
CodexHarness.apply_spawn_env(src/kiro_crew/acp/harness/codex.py:354) passes the operator'sCODEX_HOMEthrough unchanged for every runtime, including the background agentkirocrew-lite(src/kiro_crew/session_parts/policy.py:150). Nothing on main limits the MCP fleet a backgroundsession/newstarts, and the module docstring itself names the MCP fleet inCODEX_HOMEas the memory lever. No RFC or maintainer comment says background sessions should start the user's MCP servers; RFC #15748 treats the leaked processes as a defect to reclaim.Repro. From the reporter on #13324 (codex-acp 1.11.0): a recurring dashboard session on the Codex backend with MCP servers in
~/.codex/config.tomlgrew to ~855 processes / 21 GiB in 4 hours, cleared only by a restart. The background runtime opens a new codex session per title/navigation/suggestion turn, each starts the configured MCP servers, andsession/closedoes not reap them. This host has no codex-acp install, so the live before/after count below is still pending (see Manual verification).What changed (motivation → approach → change)
At background-runtime spawn,
resolve_spawnreadsCODEX_HOME/config.tomlviatomllib(stdlib ≥ 3.11;pyproject.tomlrequires ≥ 3.12) throughasyncio.to_thread, so a slow home directory never blocks the event loop. It enumerates themcp_serverskeys and stores aCODEX_CONFIGJSON override inself._bg_codex_config. A missing, unreadable or malformed file yields no override.apply_spawn_env— which runs afteragent.envis merged in — injects this asCODEX_CONFIGonly if noCODEX_CONFIGalready exists in the assembled env. Any existingCODEX_CONFIG(fromos.environ,agent.env, or cronextra_env) takes precedence; the override is then skipped and a warning is logged saying the MCP servers will start for every background session.CODEX_CONFIGis documented in the codex-acp README as "JSON object merged into the Codex session config".McpServerConfig.enabledis documented incodex-rs/config/src/mcp_types.rsline 234: "When false, Codex skips initializing this MCP server." The TOML deep-merge semantics are verified fromcodex-rs/config/src/merge.rs.Implementation follows the
opencode.py/pi.pypattern: compute inresolve_spawn, consume inapply_spawn_envviaself._bg_codex_config. No changes toSpawnPlan,launch.pyprecedence, orCODEX_HOME.Backwards compatibility
Compatible: no change to foreground sessions,
SpawnPlan,launch.py, orCODEX_HOME. Background sessions that previously had MCP servers from codex's own config will no longer have them unless the operator setsCODEX_CONFIG(in which case the operator's value wins).Removes nothing: no public API or config key is removed.
Tests
Added to
test/test_codex_sqlite_home.py:test_read_mcp_server_names_section_headers— standard[mcp_servers.<name>]headers extracted.test_read_mcp_server_names_dotted_key— dotted top-level keys extracted.test_read_mcp_server_names_dotted_in_name— a quoted server name containing a dot stays one name.test_read_mcp_server_names_malformed_toml— malformed TOML →[].test_read_mcp_server_names_empty_config/_missing_file— empty/absent →[].test_background_spawn_env_sets_codex_config_with_servers— afterresolve_spawn+apply_spawn_env, env carriesCODEX_CONFIGwithenabled:falseper server.test_background_spawn_env_does_not_overwrite_existing_codex_config— operator CODEX_CONFIG preserved and the skip is logged.test_foreground_apply_spawn_env_does_not_set_codex_config— foreground env unchanged.Reader: test/test_codex_sqlite_home.py
Manual verification
Required live check (pending owner; Design Review will clear once the result is posted):
~/.codex/config.toml:ps --ppid $(pgrep -f codex-acp) -o pid=orpgrep -P $(pgrep -f codex-acp).ps/pgrepcommand. Expected: process count stays at or near 0 (no new MCP children); title generation and navigation still work.Screenshots / video
N/A — no user-visible UI change.
Related Issues
Fixes #13324
Refs #15748
Pattern harvest
Rule candidate: agents-md
Pattern: to suppress a shared-process ACP backend's own per-session resource spawn, use the backend's documented config-override env var (
CODEX_CONFIG) with per-entryenabled:false; compute the value off-loop inresolve_spawn, store inself, inject inapply_spawn_envafter agent.env is assembled (following opencode.py/pi.py).Checklist