App-server fuzzy file search floods remote clients with intermediate snapshots
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Domain
- api, backend, performance
Research direction
Start at the app-server fuzzyFileSearch/sessionStart and sessionUpdate flow, then trace SessionReporter::on_update where snapshots become notifications. Run repro.py against the app-server binary to establish the notification count and payload size. Done means intermediate updates are coalesced and rate-limited while the latest snapshot is flushed before sessionCompleted, with the repro approaching the reported 2 updates and 8055 bytes.
Written by the indexing model from the issue text.
Description
What version of the Codex App are you using (From “About Codex” dialog)?
26.707.91948
What subscription do you have?
API
What platform is your computer?
linux
What issue are you seeing?
Summary
@ file search can be much slower through a remote Codex app connection than through the local CLI, even when both search the same checkout with the same codex-file-search backend.
The app-server streaming API sends every intermediate top-N snapshot to the client. During a large walk, the matcher can publish a changed snapshot approximately every 10 ms. App-server turns each snapshot into a separate JSON notification and async send task, with no rate limit, coalescing, or latest-only backpressure.
Observed behavior
I traced a running app-server while typing:
@path/to/my_file.py
The search used one stable fuzzyFileSearch session ID. Query updates were sent to the existing session.
For that single interaction, app-server emitted:
- 763
fuzzyFileSearch/sessionUpdatednotifications - 2.83 MiB of notification payloads
- 483 snapshots for one intermediate query prefix
- 219 snapshots for an earlier intermediate query prefix
The same query run directly with codex-file-search completed in approximately 1.4 seconds. This isolates the remaining remote-app slowdown from the underlying repository traversal.
Why this happens
The shared file-search matcher ticks every 10 ms while Nucleo reports work in progress. Whenever status.changed is true, it calls SessionReporter::on_update with a new snapshot.
The app-server reporter currently:
- Converts every snapshot into up to 50 full result objects, including match indices.
- Spawns a Tokio task for every snapshot.
- Serializes and broadcasts every notification.
This behavior is tolerable for the in-process TUI, but it is expensive across the app-server boundary. The remote client must receive, parse, reconcile, and render hundreds of mostly superseded snapshots. A slower connection or client event loop makes the backlog more visible.
Minimal reproduction
The following script creates a large temporary tree, starts the selected codex app-server binary over stdio, uses the public experimental session API, and counts update notifications until the search completes. Pass the path to the Codex binary as the optional positional argument; when omitted, the script uses codex from PATH.
#!/usr/bin/env python3
import argparse
import json
import pathlib
import subprocess
import tempfile
import time
def send(proc, message):
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()
parser = argparse.ArgumentParser()
parser.add_argument(
"codex_binary",
nargs="?",
default="codex",
help="path to the codex binary (default: codex from PATH)",
)
args = parser.parse_args()
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
# Increase this count if the local filesystem completes the walk too quickly.
for i in range(50_000):
directory = root / f"dir-{i // 1_000:03d}"
directory.mkdir(exist_ok=True)
(directory / f"needle-{i:05d}.txt").touch()
proc = subprocess.Popen(
[args.codex_binary, "app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
send(
proc,
{
"id": 0,
"method": "initialize",
"params": {
"clientInfo": {
"name": "fuzzy-search-repro",
"title": "Fuzzy Search Repro",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
},
)
while True:
message = json.loads(proc.stdout.readline())
if message.get("id") == 0:
break
send(proc, {"method": "initialized"})
send(
proc,
{
"id": 1,
"method": "fuzzyFileSearch/sessionStart",
"params": {"sessionId": "repro", "roots": [str(root)]},
},
)
# Requests for one session are serialized, so this can be pipelined behind start.
send(
proc,
{
"id": 2,
"method": "fuzzyFileSearch/sessionUpdate",
"params": {"sessionId": "repro", "query": "needle"},
},
)
started = time.monotonic()
update_count = 0
update_bytes = 0
saw_query_update = False
while True:
line = proc.stdout.readline()
if not line:
raise RuntimeError("app-server exited before search completed")
message = json.loads(line)
method = message.get("method")
params = message.get("params", {})
if (
method == "fuzzyFileSearch/sessionUpdated"
and params.get("sessionId") == "repro"
and params.get("query") == "needle"
):
saw_query_update = True
update_count += 1
update_bytes += len(line.encode())
elif (
method == "fuzzyFileSearch/sessionCompleted"
and params.get("sessionId") == "repro"
and saw_query_update
):
break
elapsed = time.monotonic() - started
print(f"updates={update_count} bytes={update_bytes} elapsed={elapsed:.3f}s")
send(
proc,
{
"id": 3,
"method": "fuzzyFileSearch/sessionStop",
"params": {"sessionId": "repro"},
},
)
proc.terminate()
Run the repro against the currently installed Codex binary with:
python repro.py /path/to/codex
On a fast local filesystem, increasing the file count or pointing one child symlink at a larger tree makes the notification flood easier to observe.
Potential fix
Coalesce notifications at the app-server boundary while leaving the shared matcher and wire protocol unchanged:
- Create one notification dispatcher task per fuzzy-search session instead of spawning one task per snapshot.
- Store only the latest pending snapshot. A newer snapshot replaces an older unsent snapshot.
- Send the first snapshot promptly, then rate-limit intermediate snapshots to approximately 10 updates per second.
- When search becomes idle, immediately flush the latest snapshot before sending
fuzzyFileSearch/sessionCompleted. - Tie completion to the query represented by the last matcher snapshot so a stale completion cannot be attributed to a newer query.
- Wake and terminate the dispatcher when the session is stopped.
This preserves incremental results and final ordering while bounding transport, serialization, task creation, and client rendering work. The rate limit should live in app-server rather than the shared matcher so the local in-process TUI can retain its current responsiveness.
After applying the fix, the repro goes from updates=31 bytes=238663 elapsed=0.433s to updates=2 bytes=8055 elapsed=0.060s.
What steps can reproduce the bug?
See above
What is the expected behavior?
No response
Additional information
codex-cli 0.144.4
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.5k
- Avg merge
- 1m
- Merged PRs (30d)
- 1k
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/codex
-
enhancement remote
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug CLI windows-os
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
macOS sandbox blocks hw.optional.arm64 sysctl, causing Flutter to misdetect Apple Silicon as x64 Openbug CLI sandbox
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug CLI TUI
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
CLI config enhancement skills
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
kwakseongjae/auto-hwp#319 ·
-
area:cli bug filter-quality good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 1/5 Under an hour Newbie friendliness 72/100
bevyengine/bevy#25861 ·
-
comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/ClickHouse#121222 ·
-
A-linter
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
oxc-project/oxc#26863 ·