Skip to content

Commit cd823f0

Browse files
committed
comments from Calvin
1 parent 031c02e commit cd823f0

File tree

3 files changed

+31
-14
lines changed

3 files changed

+31
-14
lines changed

openhands-tools/openhands/tools/terminal/definition.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,13 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
236236
"""A ToolDefinition subclass that automatically initializes a TerminalExecutor with auto-detection.""" # noqa: E501
237237

238238
def declared_resources(self, action: Action) -> DeclaredResources: # noqa: ARG002
239-
from openhands.tools.terminal.impl import TerminalExecutor
240-
241239
# When using the tmux backend, TmuxPanePool handles concurrency
242240
# internally via pane-level isolation — opt out of framework
243241
# serialization so parallel calls are allowed.
244242
# When using the subprocess backend there is only a single
245243
# session, so we declare a resource key to serialize terminal
246244
# calls against each other without blocking unrelated tools.
247-
if isinstance(self.executor, TerminalExecutor) and self.executor.is_pooled:
245+
if getattr(self.executor, "is_pooled", False):
248246
return DeclaredResources(keys=(), declared=True)
249247
return DeclaredResources(keys=("terminal:session",), declared=True)
250248

openhands-tools/openhands/tools/terminal/impl.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -361,16 +361,15 @@ def _execute_pooled(
361361
"""Execute *action* in pool mode with proper checkout/checkin.
362362
363363
All pane lifecycle (checkout, optional replace, checkin) is
364-
contained here so there is exactly one checkout and one checkin
365-
per call, and the checkin is guaranteed by ``finally``.
364+
managed by the pool's context manager so there is exactly one
365+
checkout and one checkin per call.
366366
"""
367-
terminal = self._pool.checkout() # type: ignore[union-attr]
368-
try:
367+
with self._pool.pane() as handle: # type: ignore[union-attr]
369368
reset_text: str | None = None
370369

371-
if action.reset or terminal._closed:
372-
self._discard_session(terminal)
373-
terminal = self._pool.replace(terminal) # type: ignore[union-attr]
370+
if action.reset or handle.terminal._closed:
371+
self._discard_session(handle.terminal)
372+
handle.terminal = self._pool.replace(handle.terminal) # type: ignore[union-attr]
374373
reset_text = self._RESET_TEXT
375374
logger.info(
376375
f"Terminal pane replaced (reset) working_dir: {self._working_dir}"
@@ -383,7 +382,7 @@ def _execute_pooled(
383382
exit_code=0,
384383
)
385384

386-
session = self._wrap_session(terminal)
385+
session = self._wrap_session(handle.terminal)
387386
self._prepare_pooled_session(session)
388387

389388
cmd_action = (
@@ -409,8 +408,6 @@ def _execute_pooled(
409408
)
410409

411410
return self._mask_observation(observation, conversation)
412-
finally:
413-
self._pool.checkin(terminal) # type: ignore[union-attr]
414411

415412
def __call__(
416413
self,

openhands-tools/openhands/tools/terminal/terminal/tmux_pane_pool.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import time
1111
import uuid
1212
from collections import deque
13-
from contextlib import suppress
13+
from collections.abc import Iterator
14+
from contextlib import contextmanager, suppress
1415
from dataclasses import dataclass, field
1516
from typing import Final
1617

@@ -48,6 +49,13 @@ def close(self) -> None:
4849
self._closed = True
4950

5051

52+
@dataclass(slots=True)
53+
class PaneHandle:
54+
"""Mutable handle to a checked-out pane, for use as a context manager target."""
55+
56+
terminal: PooledTmuxTerminal
57+
58+
5159
@dataclass(slots=True)
5260
class TmuxPanePool:
5361
"""Thread-safe pool of tmux panes for parallel terminal execution.
@@ -272,3 +280,17 @@ def replace(self, old_terminal: PooledTmuxTerminal) -> PooledTmuxTerminal:
272280

273281
logger.debug(f"Replaced pane {old_pane_id} -> {new_pane_id}")
274282
return new_terminal
283+
284+
@contextmanager
285+
def pane(self, timeout: float | None = None) -> Iterator[PaneHandle]:
286+
"""Context manager: checkout a pane, yield a handle, checkin on exit.
287+
288+
The yielded :class:`PaneHandle` is mutable — callers that call
289+
:meth:`replace` should assign the new terminal back to
290+
``handle.terminal`` so that the correct pane is checked in.
291+
"""
292+
handle = PaneHandle(self.checkout(timeout=timeout))
293+
try:
294+
yield handle
295+
finally:
296+
self.checkin(handle.terminal)

0 commit comments

Comments
 (0)