From 24c6970ca778910d98e2d27a4dbc7e1b443d756b Mon Sep 17 00:00:00 2001 From: T0mSIlver Date: Sat, 27 Jun 2026 00:43:45 +0000 Subject: [PATCH] fix(grep): add a subprocess timeout to ripgrep run_rg called subprocess.run with no timeout. The outer asyncio.wait_for(MAX_TOOLRUN_TIMEOUT) in ToolSet can't interrupt it because run_rg is a blocking synchronous call that holds the event loop, so a pathological regex over a large tree could hang indefinitely. Add a 10s subprocess timeout mirroring the Glob tool, and add tests for the timeout reminder and that a timeout is passed. --- src/fastcontext/agent/tool/grep.py | 8 +++++- tests/test_grep_timeout.py | 43 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/test_grep_timeout.py diff --git a/src/fastcontext/agent/tool/grep.py b/src/fastcontext/agent/tool/grep.py index 4ee161f..ffe87e1 100644 --- a/src/fastcontext/agent/tool/grep.py +++ b/src/fastcontext/agent/tool/grep.py @@ -159,7 +159,13 @@ def run_rg(rg_path: str, pattern: str, path: str, **kwargs) -> str: cwd = kwargs.get("cwd", str(Path.cwd())) - output = subprocess.run(command, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace") + timeout = 10 # seconds + try: + output = subprocess.run( + command, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout + ) + except subprocess.TimeoutExpired: + return f"Grep timed out after {timeout}s" if output.returncode == 0: output_text = ( diff --git a/tests/test_grep_timeout.py b/tests/test_grep_timeout.py new file mode 100644 index 0000000..52b6e0f --- /dev/null +++ b/tests/test_grep_timeout.py @@ -0,0 +1,43 @@ +import asyncio +import json +import subprocess +import tempfile +from pathlib import Path +from unittest import mock + +from fastcontext.agent.tool.grep import GrepTool + + +def test_grep_returns_reminder_on_timeout(): + """run_rg must bound the ripgrep subprocess and surface a system-reminder + instead of hanging / raising when it times out. (The outer asyncio + wait_for cannot interrupt the blocking subprocess.run, so the timeout + must live on the subprocess itself.)""" + grep = GrepTool() + with tempfile.TemporaryDirectory() as cwd: + (Path(cwd) / "a.txt").write_text("MATCH\n", encoding="utf-8") + + with mock.patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="rg", timeout=10)): + out = asyncio.run(grep.call(json.dumps({"pattern": "MATCH", "output_mode": "content"}), cwd=cwd)) + + assert "" in out + assert "timed out" in out + + +def test_grep_passes_timeout_to_subprocess(): + """The subprocess.run call must receive a positive timeout kwarg.""" + grep = GrepTool() + with tempfile.TemporaryDirectory() as cwd: + (Path(cwd) / "a.txt").write_text("MATCH\n", encoding="utf-8") + + real_run = subprocess.run + captured = {} + + def spy(*args, **kwargs): + captured.update(kwargs) + return real_run(*args, **kwargs) + + with mock.patch("subprocess.run", side_effect=spy): + asyncio.run(grep.call(json.dumps({"pattern": "MATCH", "output_mode": "content"}), cwd=cwd)) + + assert captured.get("timeout", 0) and captured["timeout"] > 0