Summary
V2's glob and grep tools have no default wall-clock timeout. A low-match pattern over a large search root runs unbounded — in a real session, one glob call ran for 21 minutes 41 seconds and only ended when the user manually interrupted the turn. V1 had the same missing timeout but wired ctx.abort directly into the ripgrep child process; V2's tool leaves don't pass an abort signal at all and rely solely on fiber interruption.
Environment
- opencode version: service
0.0.0-next-16305, CLI 0.0.0-next-16307 at time of incident (channel: next)
- OS: macOS 25.5.0 (arm64)
- Terminal: WarpTerminal
- Shell: /bin/zsh
- Install/channel: next (managed background service)
- Active plugins: one local plugin (
voice.ts), not involved in the tool path
Reproduction
- In a session whose agent has the built-in
glob/grep tools, ask for a search that has few matches under a large root, e.g. glob with pattern: "**/*.metal", path: "/Users/<user>/code" where that directory contains dozens of repos with large node_modules/target/dist trees between gitignore contexts.
- Because matches never reach
limit + 1, Stream.take(limit + 1) never short-circuits and ripgrep must walk the entire tree.
- Nothing bounds wall-clock time. The tool call stays pending indefinitely; the session shows no progress and queued steer messages wait behind it.
Expected Behavior
Search tools should have a bounded default execution time (the shell tool defaults to 120s). Ideally a timeout returns the partial results collected so far with a note like (Search timed out after 30s — results are partial. Narrow the path or pattern.) rather than an error.
Actual Behavior
Observed incident timeline (from the session event store):
| Time (UTC) |
Event |
| 19:38:44 |
glob **/*.metal /Users/kit/code, glob **/*{Hex,HEX,hex}*, and a broad grep called in one batch |
| 19:38:56 |
A find-based shell call in the same batch returns in 12s |
| 19:47:51 |
User steer message admitted — does not preempt; queues behind the wedged tools |
| 20:00:25 |
User interrupts; all three tools recorded as session.tool.failed / aborted ("Tool execution interrupted") |
Total: 21m41s of pending tool execution with zero output, plus a 12.5-minute queued user message.
Additional Context
Code analysis on origin/v2 (packages/core/src/ripgrep.ts, packages/core/src/tool/plugin/{glob,grep}.ts):
Ripgrep.Service's GlobInput/GrepInput already accept an optional signal?: AbortSignal (raced via waitForAbort), but the glob/grep tool leaves never pass one — they call ripgrep.glob({ cwd, pattern, limit }) bare.
- There is no
Effect.timeout/deadline anywhere on the run program in Ripgrep.Service.
- Early termination exists only via
Stream.take(input.limit + 1), i.e. only when the search finds enough matches. Low-match patterns get zero benefit.
- For comparison, v1 (
dev: packages/opencode/src/tool/glob.ts:42, grep.ts:52) plumbed ctx.abort into the ripgrep child directly.
Proposed fix: add a default deadline to Ripgrep.Service.run (bounded, e.g. 30s) that terminates the child and returns partial items with a timedOut/partial marker the tool leaves render as a truncation note. Happy to open a PR with a reproducer test (fake AppProcess whose stdout never closes) and the fix.
Related but separate: a steer-delivery user message admitted mid-turn did not preempt the wedged tool calls and sat queued for 12+ minutes; that likely deserves its own issue.
cc @rekram1-node
Summary
V2's
globandgreptools have no default wall-clock timeout. A low-match pattern over a large search root runs unbounded — in a real session, oneglobcall ran for 21 minutes 41 seconds and only ended when the user manually interrupted the turn. V1 had the same missing timeout but wiredctx.abortdirectly into the ripgrep child process; V2's tool leaves don't pass an abort signal at all and rely solely on fiber interruption.Environment
0.0.0-next-16305, CLI0.0.0-next-16307at time of incident (channel: next)voice.ts), not involved in the tool pathReproduction
glob/greptools, ask for a search that has few matches under a large root, e.g.globwithpattern: "**/*.metal",path: "/Users/<user>/code"where that directory contains dozens of repos with largenode_modules/target/disttrees between gitignore contexts.limit + 1,Stream.take(limit + 1)never short-circuits and ripgrep must walk the entire tree.Expected Behavior
Search tools should have a bounded default execution time (the
shelltool defaults to 120s). Ideally a timeout returns the partial results collected so far with a note like(Search timed out after 30s — results are partial. Narrow the path or pattern.)rather than an error.Actual Behavior
Observed incident timeline (from the session event store):
glob **/*.metal /Users/kit/code,glob **/*{Hex,HEX,hex}*, and a broadgrepcalled in one batchfind-basedshellcall in the same batch returns in 12ssession.tool.failed/aborted("Tool execution interrupted")Total: 21m41s of pending tool execution with zero output, plus a 12.5-minute queued user message.
Additional Context
Code analysis on
origin/v2(packages/core/src/ripgrep.ts,packages/core/src/tool/plugin/{glob,grep}.ts):Ripgrep.Service'sGlobInput/GrepInputalready accept an optionalsignal?: AbortSignal(raced viawaitForAbort), but the glob/grep tool leaves never pass one — they callripgrep.glob({ cwd, pattern, limit })bare.Effect.timeout/deadline anywhere on therunprogram inRipgrep.Service.Stream.take(input.limit + 1), i.e. only when the search finds enough matches. Low-match patterns get zero benefit.dev:packages/opencode/src/tool/glob.ts:42,grep.ts:52) plumbedctx.abortinto the ripgrep child directly.Proposed fix: add a default deadline to
Ripgrep.Service.run(bounded, e.g. 30s) that terminates the child and returns partial items with atimedOut/partialmarker the tool leaves render as a truncation note. Happy to open a PR with a reproducer test (fakeAppProcesswhose stdout never closes) and the fix.Related but separate: a steer-delivery user message admitted mid-turn did not preempt the wedged tool calls and sat queued for 12+ minutes; that likely deserves its own issue.
cc @rekram1-node