You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sibling to #242/#243, but a different lane — and the reason I'm filing rather than reopening: every census in those two issues was SerialLspStatus. This one is PureRead, which v0.51.3's deferred-response work does not cover. The deployed fix is not regressing; this is a second hole in the same wall.
What happened
A subagent session was working in a scratch tree while its actor root was ~/projects/opencode. One interactive read job stopped returning and never came back:
14 censuses over 13 minutes, still live when I intervened. Every phase that does work is 0ms; the entire wait is ack_ready, behind the read gate.
Client-side this rendered as module_id 'aft' did not answer route.bind within 12s, repeatedly. The subagent diagnosed itself correctly before giving up: "after a Playwright command timed out, every subsequent AFT/bash/search call fails."
The job survived its client. The operator killed the process; the daemon-side job kept running. Only a drain-restart of the module cleared it.
It is a runaway, not a leaked in-flight counter
I checked, because the fixes differ:
tid 2083 aft-executor-wo 512 CPU ticks / 5s ≈ 100% of one core
process disk read 0 KB / 3s
D-state threads none
daemon RSS 0.77 GB, 1d18h uptime — otherwise healthy
One executor worker pinned, no blocked threads. Something is genuinely executing, not a flag that failed to decrement.
Note on the 0 KB: that rules out disk I/O, not a filesystem walk — the tree had just been written by a test run, so it was entirely in page cache. I initially read it as proof of pure compute and that was overreach.
The target
The searches immediately before the wedge were aft_search calls with an explicit path into a scratch tree:
/tmp/oc-diffux 148,732 files 2.8 GB NOT a git root 2,408 node_modules dirs
Served through a borrowed search index — the journal shows borrowed search index for /tmp/oc-diffux has 0 drifted file(s) repeatedly up to 11:42:04, with searches completing normally in 100–800ms. Then a Playwright e2e run (large file churn across that tree) errored, a bash_kill landed at 11:45:01, and the very next job on that channel wedged.
Why the existing guards didn't hold
MAX_FALLBACK_WALK_FILES (50,000) and FALLBACK_WALK_BUDGET (10s) exist and are wired into this exact call — commands/semantic_search.rs:560-561 passes both into search_grep_bounded. The job ran ~260× the walk budget regardless, so whatever consumed the time is not inside the bounded walk.
One negative result that narrows it: I reproduced the same query against the same tree with a standalone binary and an isolated storage dir. It returned in 0 seconds — but with Semantic search is not enabled, so it took the lexical path only. Plain lexical search over that tree is not the culprit.
What I could not determine
I could not localize the hot loop. The release binary is stripped, so eu-stack returned nothing, and strace isn't installed on this host. Two hypotheses I can't separate with what I have:
Borrowed-index drift recomputation. The index reported 0 drifted files right up until a test run churned a large fraction of a 148k-file tree. Recomputing that delta could be both unbounded and CPU-shaped.
A cold index/semantic build triggered by that drift, with the interactive PureRead job parked waiting on the artifact — in which case the pinned thread is the build and the stuck job is a waiter holding the gate. This would fit the evidence equally well, and it changes the fix.
A debug build would separate these in minutes. Happy to run anything specific on this host; the tree still exists and the shape reproduces on demand.
Why this matters beyond one wedged session
The failure mode is worse than slow. A single interactive read on the PureRead lane can:
run unbounded, with no aggregate command timeout
hold the read gate, so has_epoch_reader blocks Lane::Mutating — every write and every new bind to that root queues behind it indefinitely
survive the death of the client that issued it
be clearable only by restarting the daemon, which drops warm state for every other root
The started_before_oldest_writer=true field you added for #243 is doing exactly its job here: it says the promotion gate is working and simply cannot preempt an already-running reader. The gap is not admission fairness — it's that nothing bounds or cancels the reader.
ICEY1W32's report is the same root-type problem in a different subsystem: a large non-git directory full of dependency trees is a poor candidate for eager whole-tree work. Your stated direction there — "for such roots the callgraph moves to on-demand" — would want to cover external-path search too, not just the eager callgraph build. Ours was 148k files against their 24k.
The two pieces that would have contained this independently of root-type policy:
An aggregate bound on interactive lane occupancy, so no single read can hold the gate indefinitely regardless of what it's doing internally.
Sibling to #242/#243, but a different lane — and the reason I'm filing rather than reopening: every census in those two issues was
SerialLspStatus. This one isPureRead, which v0.51.3's deferred-response work does not cover. The deployed fix is not regressing; this is a second hole in the same wall.What happened
A subagent session was working in a scratch tree while its actor root was
~/projects/opencode. One interactive read job stopped returning and never came back:14 censuses over 13 minutes, still live when I intervened. Every phase that does work is 0ms; the entire wait is
ack_ready, behind the read gate.Client-side this rendered as
module_id 'aft' did not answer route.bind within 12s, repeatedly. The subagent diagnosed itself correctly before giving up: "after a Playwright command timed out, every subsequent AFT/bash/search call fails."The job survived its client. The operator killed the process; the daemon-side job kept running. Only a drain-restart of the module cleared it.
It is a runaway, not a leaked in-flight counter
I checked, because the fixes differ:
One executor worker pinned, no blocked threads. Something is genuinely executing, not a flag that failed to decrement.
Note on the 0 KB: that rules out disk I/O, not a filesystem walk — the tree had just been written by a test run, so it was entirely in page cache. I initially read it as proof of pure compute and that was overreach.
The target
The searches immediately before the wedge were
aft_searchcalls with an explicitpathinto a scratch tree:Served through a borrowed search index — the journal shows
borrowed search index for /tmp/oc-diffux has 0 drifted file(s)repeatedly up to 11:42:04, with searches completing normally in 100–800ms. Then a Playwright e2e run (large file churn across that tree) errored, abash_killlanded at 11:45:01, and the very next job on that channel wedged.Why the existing guards didn't hold
MAX_FALLBACK_WALK_FILES(50,000) andFALLBACK_WALK_BUDGET(10s) exist and are wired into this exact call —commands/semantic_search.rs:560-561passes both intosearch_grep_bounded. The job ran ~260× the walk budget regardless, so whatever consumed the time is not inside the bounded walk.One negative result that narrows it: I reproduced the same query against the same tree with a standalone binary and an isolated storage dir. It returned in 0 seconds — but with
Semantic search is not enabled, so it took the lexical path only. Plain lexical search over that tree is not the culprit.What I could not determine
I could not localize the hot loop. The release binary is stripped, so
eu-stackreturned nothing, andstraceisn't installed on this host. Two hypotheses I can't separate with what I have:PureReadjob parked waiting on the artifact — in which case the pinned thread is the build and the stuck job is a waiter holding the gate. This would fit the evidence equally well, and it changes the fix.A debug build would separate these in minutes. Happy to run anything specific on this host; the tree still exists and the shape reproduces on demand.
Why this matters beyond one wedged session
The failure mode is worse than slow. A single interactive read on the
PureReadlane can:has_epoch_readerblocksLane::Mutating— every write and every new bind to that root queues behind it indefinitelyThe
started_before_oldest_writer=truefield you added for #243 is doing exactly its job here: it says the promotion gate is working and simply cannot preempt an already-running reader. The gap is not admission fairness — it's that nothing bounds or cancels the reader.Adjacent to #250
ICEY1W32's report is the same root-type problem in a different subsystem: a large non-git directory full of dependency trees is a poor candidate for eager whole-tree work. Your stated direction there — "for such roots the callgraph moves to on-demand" — would want to cover external-path search too, not just the eager callgraph build. Ours was 148k files against their 24k.
The two pieces that would have contained this independently of root-type policy:
PureReadwhose route is gone has no reason to keep computing.