Skip to content

[Bug][Windows]: v2 guidance catalog-state probe still costs ~7.5s per turn (the #1876 async fix removed the event-loop block, not the request latency) #2499

Description

@Vladimir321123

Client or integration

Codex App, Codex CLI, and any ACP client (observed via @agentclientprotocol/codex-acp inside Obsidian Copilot).

Area

Proxy and routing / Platform (Windows)

Summary

#1852 reported that the Windows catalog-state enumeration blocked the Bun event loop. #1876 fixed that: collectCodexAppServerCatalogStateForRequest() now uses listWindowsSnapshotsAsync() with in-flight dedup and a short cache, and /healthz stays responsive.

The latency half of the same code path is still there, and it is now the dominant cost of every multi-agent v2 turn. On 2.32.0 the enumeration still runs on the request hot path, and on this machine it takes ~7.5s — longer than CATALOG_STATE_TTL_MS, so the cache almost never serves a request.

Measured on this installation, same account, same trivial prompt ("reply with PONG"), 4 runs each, interleaved:

Route median runs (ms)
through proxy 11296 ms 10844, 11245, 11296, 12689
direct to ChatGPT (no proxy) 5151 ms 3818, 4654, 5151, 5836
through proxy, multiAgentGuidanceEnabled: false 3424 ms 3118, 3292, 3424, 5698

So the proxy was ~7s slower than no proxy at all; with guidance off it is faster than direct (the WS upstream transport pays off as documented in ws-upstream.ts).

Evidence that the cost is the catalog probe, not the network

  1. The proxy's own usage.jsonl already shows it. Representative slow row:
durationMs: 10175, firstOutputMs: 9969
attempts: [{ ordinal: 1, adapter: "openai-responses", status: 200,
             durationMs: 2024, firstOutputMs: 1818, sendCount: 1, recoveryKinds: [] }]

The upstream attempt took 2.0s; the request took 10.2s. ~8s is spent before the attempt starts.

  1. CPU time of the proxy process during those 8 seconds is 0–31 ms per 900 ms sample — it is waiting on a child process, not computing.

  2. Temporary console.log timestamps in src/server/responses/core.ts localise it exactly:

core: enter                       t0
step: before body read            t0 +8 ms
step: before route normalization  t0 +17 ms
step: before codex auth           t0 +7492 ms     <-- applyFinalRouteRequestNormalization
step: before request build        t0 +7497 ms
step: before fetch                t0 +7501 ms
providerFetch -> WS upstream      t0 +7502 ms
wsUpstream OPEN                   +346 ms
first frame to client             +498 ms

applyFinalRouteRequestNormalization()multiAgentGuidanceText()defaultCollectCatalogState() is the whole delay. With multiAgentGuidanceEnabled: false the same steps take 1–2 ms and durationMs equals attempts[0].durationMs within 4–8 ms.

Root cause: GetOwner is per-process, and the TTL is shorter than the probe

Running the exact script from windowsSnapshotPowerShellCommand() standalone:

snapshot #1: 5617 ms, 9 rows
snapshot #2: 5909 ms, 9 rows
snapshot #3: 5893 ms, 10 rows

Split into its two parts (822 processes on this machine):

Step Cost
Get-CimInstance Win32_Process + CommandLine regex filter 646 ms
Invoke-CimMethod GetOwner, 93 candidate processes 43887 ms (~472 ms per process)

So the snapshot cost scales with the number of running Codex processes, not with machine size. This installation runs 9–10 of them at once — desktop app-server, plugin app-server, codex sandbox, codex-code-mode-host, codex-command-runner — which is an ordinary Codex App session, giving ~5.9s; the batched start-time query brings the total to ~7.5s.

That interacts badly with the cache:

const CATALOG_STATE_TTL_MS = 5_000;          // app-server-processes.ts
const CATALOG_STATE_UNKNOWN_TTL_MS = 250;

A 5s TTL in front of a 7.5s probe cannot help: the entry is stale before the next turn starts. In-flight dedup only merges concurrent turns, and sequential chat turns are not concurrent. The comment above collectCodexAppServerCatalogStateForRequest ("Typical cold cost is tens of milliseconds") does not hold on Windows once more than one or two Codex processes are alive.

Suggestion 5 from #1852 ("optionally narrow the CIM query before owner lookups") was reasonably deferred there, because narrowing does not fix event-loop blocking. Now that the blocking is fixed, that suggestion is the remaining problem.

Suggested safe direction

  1. Drop the per-process GetOwner fan-out. One Get-CimInstance Win32_Process already returns everything needed except the owner; owner can come from a single association query, or from a SessionId/token comparison, or by dropping owner filtering to a cheaper heuristic and treating ambiguity as unknown. This alone should turn ~5.9s into <1s.
  2. Serve stale while revalidating. The probe result is advisory. Returning the last known state immediately and refreshing in the background would remove it from the hot path entirely, instead of making one unlucky turn per TTL window pay the full cost.
  3. Make the TTL exceed the observed probe cost (or adapt it to the measured duration). A TTL below the probe duration guarantees a miss on every sequential turn.
  4. Regression test: with the enumeration seam stubbed to take, say, 3s and a TTL of 5s, assert that N sequential guidance builds trigger at most one enumeration and that turns after the first do not wait on it.

Workaround

ocx agent injection set --guidance off

(equivalently "multiAgentGuidanceEnabled": false in ~/.opencodex/config.json). Median turn dropped from 11296 ms to 3424 ms. This only disables OpenCodex's extra guidance injection; syncCodexSubagentDefaults and Codex's own collaboration tools are unaffected.

Note that this is the effective default for every user, since multiAgentGuidanceEnabled(config) returns config.multiAgentGuidanceEnabled !== false.

Version

OpenCodex 2.32.0 (verified byte-identical to the published npm tarball), bundled Bun 1.4.0, Codex CLI 0.149.0.

Operating system

Windows 11 Home, version 10.0.26200 (build 26200). Native Windows, no WSL.

Provider and model

Not provider-specific. Reproduced on openai/gpt-5.6-sol with ChatGPT login (codexAccountMode: pool); the delay happens before any provider work.

Logs or error output

usage.jsonl (slow turn)   durationMs=10175  firstOutputMs=9969
                          attempts[0].durationMs=2024  firstOutputMs=1818
proxy process CPU during the 8s gap: 0-31 ms per 900 ms sample
standalone snapshot script: 5617 / 5909 / 5893 ms
GetOwner alone, 93 processes: 43887 ms

Screenshots and supporting files

Not required; timings and code paths above are text-reproducible.

Redacted configuration

{
  "multiAgentGuidanceEnabled": true,
  "injectionModel": "gpt-5.6-sol",
  "injectionEffort": "xhigh"
}

Related issues

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcatalogModel catalog, slugs, visibility, routed entriesplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)streamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions