Synchronous tools block turn_loop cancellation
Several built-in tools declare async fn execute(...) but call synchronous, potentially long-running I/O inside the async task. This blocks the tokio scheduler and prevents the engine's turn loop from observing CancellationToken updates — user-facing cancel buttons / Op::Cancel are effectively no-ops until the tool finishes naturally.
Affected tools (audit)
| Tool |
Synchronous operation |
Worst case observed |
file_search |
ignore::WalkBuilder full traversal |
minutes on large dirs |
grep_files |
walker + per-file fs::read_to_string + regex |
minutes |
list_dir |
single-level fs::read_dir |
usually <1s, edge cases possible |
Short-term workaround (per-tool)
spawn_blocking + tokio::time::timeout(30s) wrapper. PR #1790 applies this to file_search. The same pattern applies cleanly to grep_files.
Pros: restores cancel responsiveness, single-file change, no API impact.
Cons: orphaned blocking task keeps running until natural completion (acceptable in practice — user-facing cancellation works, orphan eventually finishes without holding shared state).
Long-term proposal: expose cancel_token through ToolContext
Adding pub cancel_token: CancellationToken to ToolContext would let synchronous tools:
- Periodically check
cancel_token.is_cancelled() between iterations (cheap if checked every N entries, e.g. every 100 walker entries)
- Use
tokio::select! to race blocking work against cancellation
- Abort mid-iteration when cancelled, not just at natural completion
API impact:
- One field added to
ToolContext
- Propagated through tool spec / runtime construction
- Each affected tool's
execute updated to use it incrementally — old tools that don't read the field continue to work
Happy to draft a concrete API proposal if there's interest.
User-facing workaround (no upstream fix yet)
- Avoid
file_search / grep_files against $HOME or other large trees
- Use
path parameter to constrain searches
- Set narrow
extensions / include filters
Synchronous tools block turn_loop cancellation
Several built-in tools declare
async fn execute(...)but call synchronous, potentially long-running I/O inside the async task. This blocks the tokio scheduler and prevents the engine's turn loop from observingCancellationTokenupdates — user-facing cancel buttons /Op::Cancelare effectively no-ops until the tool finishes naturally.Affected tools (audit)
file_searchignore::WalkBuilderfull traversalgrep_filesfs::read_to_string+ regexlist_dirfs::read_dirShort-term workaround (per-tool)
spawn_blocking + tokio::time::timeout(30s)wrapper. PR #1790 applies this tofile_search. The same pattern applies cleanly togrep_files.Pros: restores cancel responsiveness, single-file change, no API impact.
Cons: orphaned blocking task keeps running until natural completion (acceptable in practice — user-facing cancellation works, orphan eventually finishes without holding shared state).
Long-term proposal: expose
cancel_tokenthroughToolContextAdding
pub cancel_token: CancellationTokentoToolContextwould let synchronous tools:cancel_token.is_cancelled()between iterations (cheap if checked every N entries, e.g. every 100 walker entries)tokio::select!to race blocking work against cancellationAPI impact:
ToolContextexecuteupdated to use it incrementally — old tools that don't read the field continue to workHappy to draft a concrete API proposal if there's interest.
User-facing workaround (no upstream fix yet)
file_search/grep_filesagainst$HOMEor other large treespathparameter to constrain searchesextensions/includefilters