Skip to content

Commit 6a1f7c8

Browse files
authored
fix(evals): close inherited stdin in SWE-bench sandboxes (#2303)
Backport SWE-ReX #281 semantics at the mini-swe-agent Modal adapter so stdin-reading commands receive EOF without breaking pipes or heredocs. Keep the existing 300-second command timeout and add regression coverage. 中文:修复 SWE-bench 沙箱继承服务端标准输入后,cat、grep 等命令无输入时持续阻塞的问题。在 mini-swe-agent Modal 适配层回移 SWE-ReX #281 的语义,使此类命令立即收到 EOF,同时保留管道和 heredoc 行为;维持现有 300 秒命令超时并补充回归测试。
1 parent b38bf41 commit 6a1f7c8

4 files changed

Lines changed: 89 additions & 5 deletions

File tree

benchmarks/benchmark_lib.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,7 +1113,7 @@ _install_swebench_agent_deps() {
11131113
python3 -m pip install -q --no-cache-dir --break-system-packages \
11141114
'mini-swe-agent==2.4.5' 'swe-rex[modal]==1.4.0' || true
11151115
_patch_swebench_agent || \
1116-
echo "WARN: mini-swe-agent/swe-rex patches failed; sandboxes will leak until runtime_timeout and budget-exhausted instances will submit nothing" >&2
1116+
echo "WARN: mini-swe-agent/swe-rex patches failed; sandbox cleanup, submission fallback, or non-interactive stdin handling may be degraded" >&2
11171117
}
11181118

11191119
_patch_swebench_agent() {

utils/evals/EVALS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ Source rewrites are anchor-checked, idempotent, and atomic.
174174
compatibility (no `{"type": "text"}` injection for non-HF tokenizers).
175175
Copied into a temp dir as `sitecustomize.py` on `PYTHONPATH`.
176176
- `patch_swebench_agent.py` (`_patch_swebench_agent`): mini-swe-agent/swe-rex
177-
sandbox lifecycle cleanup + budget-exhaustion submission fallback.
177+
sandbox lifecycle cleanup, budget-exhaustion submission fallback, and the
178+
[SWE-ReX #281](https://github.com/SWE-agent/SWE-ReX/pull/281) closed-stdin fix.
178179
- `patch_swebench_scoring.py` (`_patch_swebench_scoring`): swebench Modal
179180
scorer reserved-CPU reduction + sandbox termination on instance completion.
180181

@@ -199,8 +200,9 @@ append_lm_eval_summary
199200
local endpoint, each instance's shell running in a Modal sandbox via swe-rex — the real
200201
SWE-bench setting) or `single-shot` (lm-eval, one prompt per instance — a ~10% floor baseline,
201202
kept only as an explicit debugging escape hatch). Agentic knobs: `SWEBENCH_AGENT_WORKERS`
202-
(default: the config's `CONC`, else 64), `SWEBENCH_AGENT_STEP_LIMIT` (75), `SWEBENCH_AGENT_TIMEOUT`
203-
(4h), `SWEBENCH_AGENT_SANDBOX_CPU` (unset = Modal default), `SWEBENCH_MODAL_APP_NAME`
203+
(default: the config's `CONC`, else 64), `SWEBENCH_AGENT_STEP_LIMIT` (75),
204+
`SWEBENCH_AGENT_CMD_TIMEOUT` (per command, 300s), `SWEBENCH_AGENT_TIMEOUT` (4h),
205+
`SWEBENCH_AGENT_SANDBOX_CPU` (unset = Modal default), and `SWEBENCH_MODAL_APP_NAME`
204206
(`infx-evals-swe`).
205207
- Run size: `EVAL_LIMIT` empty runs the full ~300-instance split; a positive integer runs the
206208
first N as an explicit smoke-test slice. `EVAL_LIMIT=full` (or `0`) also selects the full split.

utils/evals/patches/patch_swebench_agent.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,27 @@ def _patch(path: str, replacements: list[tuple[str, str, str]], label: str) -> b
4040
return True
4141

4242

43+
44+
def _patch_swerex_environment(path: str) -> bool:
45+
return _patch(
46+
path,
47+
[
48+
(
49+
' command = action.get("command", "") if isinstance(action, dict) else action\n'
50+
" try:",
51+
' command = action.get("command", "") if isinstance(action, dict) else action\n'
52+
' command = f"exec </dev/null\\n{command}" # close inherited server stdin (SWE-ReX #281)\n'
53+
" try:",
54+
"# close inherited server stdin (SWE-ReX #281)",
55+
),
56+
],
57+
"swebench-agentic",
58+
)
59+
4360
def main() -> int:
4461
import minisweagent.run.benchmarks.swebench as mini_swebench
4562
import swerex.deployment.modal as rex_modal
63+
import minisweagent.environments.extra.swerex_modal as mini_rex_modal
4664

4765
mini_ok = _patch(
4866
mini_swebench.__file__,
@@ -100,6 +118,8 @@ def main() -> int:
100118
],
101119
"swebench-agentic",
102120
)
121+
mini_env_ok = _patch_swerex_environment(mini_rex_modal.__file__)
122+
103123

104124
app_name = os.environ.get("SWEBENCH_MODAL_APP_NAME", "infx-evals-swe")
105125
rex_ok = _patch(
@@ -137,7 +157,7 @@ def main() -> int:
137157
],
138158
"swebench-agentic",
139159
)
140-
return 0 if mini_ok and rex_ok else 1
160+
return 0 if mini_ok and mini_env_ok and rex_ok else 1
141161

142162

143163
if __name__ == "__main__":

utils/evals/test_eval_patches.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib.util
22
import json
33
import runpy
4+
import subprocess
45
import sys
56
import types
67
from pathlib import Path
@@ -42,6 +43,67 @@ def test_agent_patch_is_atomic_and_idempotent(tmp_path):
4243
assert target.read_text() == patched
4344

4445

46+
def test_agent_patch_closes_inherited_stdin(tmp_path):
47+
target = tmp_path / "swerex_modal.py"
48+
target.write_text(
49+
"""import json
50+
import subprocess
51+
52+
class Environment:
53+
def execute(self, action):
54+
command = action.get("command", "") if isinstance(action, dict) else action
55+
try:
56+
result = subprocess.run(
57+
command,
58+
shell=True,
59+
timeout=0.2,
60+
stdout=subprocess.PIPE,
61+
text=True,
62+
)
63+
except subprocess.TimeoutExpired:
64+
return {"returncode": -1, "output": "timed out"}
65+
return {"returncode": result.returncode, "output": result.stdout}
66+
67+
environment = Environment()
68+
commands = ["cat", "printf 'pipe-ok\\\\n' | cat"]
69+
print(json.dumps([environment.execute({"command": command}) for command in commands]))
70+
"""
71+
)
72+
73+
assert agent_patch._patch_swerex_environment(str(target))
74+
patched = target.read_text()
75+
assert agent_patch._patch_swerex_environment(str(target))
76+
assert target.read_text() == patched
77+
78+
process = subprocess.Popen(
79+
[sys.executable, str(target)],
80+
stdin=subprocess.PIPE,
81+
stdout=subprocess.PIPE,
82+
stderr=subprocess.PIPE,
83+
text=True,
84+
)
85+
try:
86+
assert process.wait(timeout=2) == 0
87+
finally:
88+
if process.poll() is None:
89+
process.kill()
90+
process.wait()
91+
assert process.stdin is not None
92+
process.stdin.close()
93+
94+
assert process.stdout is not None
95+
assert process.stderr is not None
96+
output = process.stdout.read()
97+
errors = process.stderr.read()
98+
process.stdout.close()
99+
process.stderr.close()
100+
assert not errors
101+
assert json.loads(output) == [
102+
{"returncode": 0, "output": ""},
103+
{"returncode": 0, "output": "pipe-ok\n"},
104+
]
105+
106+
45107
def test_scoring_patch_is_atomic_and_idempotent(tmp_path):
46108
target = tmp_path / "run_evaluation_modal.py"
47109
target.write_text("prefix\ncpu=4,\nsuffix\n")

0 commit comments

Comments
 (0)