Problem
Over the standalone NDJSON transport, a semantic_search request can neither be cancelled by the client nor avoided blocking sibling requests.
1. Search runs inline on the single request thread
The NDJSON main loop dispatches synchronously (crates/aft/src/main.rs:314, dispatch_outcome called inline). With AFT_WAIT_FOR_SEMANTIC_READY=1 (set by harness plugins so the first search doesn't return partial results), wait_for_semantic_index_before_search (main.rs:914-953) sleeps in a loop for up to AFT_WAIT_FOR_SEMANTIC_READY_MS (600s in the pi plugin) while the index builds. During that window every other command — status polls, outline, other tool calls — queues behind it, because they all share the one request thread. A big-repo cold build (~2h) means every search during the build hangs ~10 minutes and then errors with semantic_index_timeout, and takes the rest of the bridge down with it.
2. Cancellation infra exists but isn't reachable standalone
search_cancellation_requested() (crates/aft/src/commands/semantic_search.rs:191 → executor current_job_cancelled) is only wired for the subc/executor job path (route-closed cancellation). There is no wire-level cancel command for NDJSON: when the client gives up, the child keeps waiting/embedding to completion. Harness-side, the best a client can do is abandon the request locally — the search keeps occupying the request thread.
Proposal
- Route long-running read commands (
semantic_search, maybe others) through deferred dispatch on the existing executor so JobCancellation applies; keep the response delivered via the pending-response path already used by inspect.
- Add a wire
cancel_request { id } command for standalone so clients can propagate user aborts.
- Honor cancellation in
wait_for_semantic_index_before_search (wake the 250ms sleep loop on cancel) and in the query-embed path.
Even without (2), (1) alone removes the collateral damage: a stuck search would no longer block status polls and sibling tool calls.
Problem
Over the standalone NDJSON transport, a
semantic_searchrequest can neither be cancelled by the client nor avoided blocking sibling requests.1. Search runs inline on the single request thread
The NDJSON main loop dispatches synchronously (crates/aft/src/main.rs:314,
dispatch_outcomecalled inline). WithAFT_WAIT_FOR_SEMANTIC_READY=1(set by harness plugins so the first search doesn't return partial results),wait_for_semantic_index_before_search(main.rs:914-953) sleeps in a loop for up toAFT_WAIT_FOR_SEMANTIC_READY_MS(600s in the pi plugin) while the index builds. During that window every other command —statuspolls,outline, other tool calls — queues behind it, because they all share the one request thread. A big-repo cold build (~2h) means every search during the build hangs ~10 minutes and then errors withsemantic_index_timeout, and takes the rest of the bridge down with it.2. Cancellation infra exists but isn't reachable standalone
search_cancellation_requested()(crates/aft/src/commands/semantic_search.rs:191 → executorcurrent_job_cancelled) is only wired for the subc/executor job path (route-closed cancellation). There is no wire-level cancel command for NDJSON: when the client gives up, the child keeps waiting/embedding to completion. Harness-side, the best a client can do is abandon the request locally — the search keeps occupying the request thread.Proposal
semantic_search, maybe others) through deferred dispatch on the existing executor soJobCancellationapplies; keep the response delivered via the pending-response path already used byinspect.cancel_request { id }command for standalone so clients can propagate user aborts.wait_for_semantic_index_before_search(wake the 250ms sleep loop on cancel) and in the query-embed path.Even without (2), (1) alone removes the collateral damage: a stuck search would no longer block
statuspolls and sibling tool calls.