Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions tests/tools/test_process_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_heartbeat_carries_only_new_output_and_stops_at_exit(tmp_path, monkeypat
timeout=2.5)


def test_terminal_dispatch_heartbeat_implies_notify_and_refuses_foreground(monkeypatch):
def test_terminal_dispatch_normalizes_materialized_background_fields_for_foreground(monkeypatch):
from tools import terminal_tool as tt

captured = {}
Expand All @@ -74,8 +74,17 @@ def fake_terminal_tool(**kwargs):

monkeypatch.setattr(tt, "terminal_tool", fake_terminal_tool)
dispatch = tt._handle_terminal
fg = json.loads(dispatch({"command": "sleep 1", "heartbeat": 120}))
assert fg.get("error") and "background" in fg["error"]
fg = json.loads(dispatch({
"command": "sleep 1",
"background": False,
"notify": False,
"heartbeat": 60,
}))
assert "error" not in fg or not fg["error"]
assert captured["heartbeat"] == 0 and captured["notify_on_complete"] is False

foreground_notify = json.loads(dispatch({"command": "sleep 1", "notify": True}))
assert foreground_notify.get("error") and "background" in foreground_notify["error"]

bg = json.loads(dispatch({"command": "sleep 1", "background": True, "heartbeat": 120}))
assert "error" not in bg or not bg["error"]
Expand Down
8 changes: 5 additions & 3 deletions tools/terminal_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,21 +1411,23 @@ def _handle_terminal(args, **kw):
)
# `notify` is the advertised interface (true → notify_on_complete,
# [...] → watch_patterns); the legacy args stay accepted, explicit
# `notify` wins. Background-only modifiers on a foreground call fail
# with the corrected call instead of being silently ignored.
# `notify` wins. Tool schemas mechanically materialize false background
# defaults and a heartbeat for ordinary foreground calls, so discard those
# non-operative fields instead of making the model retry the same command.
notify = args.get("notify")
notify_on_complete = args.get("notify_on_complete", False)
watch_patterns = args.get("watch_patterns")
heartbeat = args.get("heartbeat") or 0
if not isinstance(heartbeat, int) or isinstance(heartbeat, bool) or heartbeat < 0:
return tool_error("heartbeat must be a whole number of seconds (min 60).")
if not args.get("background", False):
if notify or watch_patterns or notify_on_complete or heartbeat:
if notify or watch_patterns or notify_on_complete:
return tool_error(
"notify/heartbeat only apply to background commands (foreground "
"results return directly). Either drop them, or run as "
"terminal(command=..., background=true, notify=...)."
)
heartbeat = 0
if args.get("pty", False):
return tool_error(
"pty requires background=true (a PTY session is interacted "
Expand Down