openhands-tools 1.54.0__tar.gz → 1.55.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/PKG-INFO +2 -1
  2. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/impl.py +6 -1
  3. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/definition.py +4 -4
  4. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/manager.py +10 -2
  5. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/definition.py +21 -2
  6. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/constants.py +15 -1
  7. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/terminal_session.py +4 -2
  8. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +7 -8
  9. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_terminal.py +75 -7
  10. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/PKG-INFO +2 -1
  11. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/requires.txt +1 -0
  12. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/pyproject.toml +2 -1
  13. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/__init__.py +0 -0
  14. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/__init__.py +0 -0
  15. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/core.py +0 -0
  16. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/definition.py +0 -0
  17. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  18. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/definition.py +0 -0
  19. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/impl.py +0 -0
  20. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/__init__.py +0 -0
  21. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/definition.py +0 -0
  22. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/event_storage.py +0 -0
  23. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/impl.py +0 -0
  24. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  25. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/recording.py +0 -0
  26. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/server.py +0 -0
  27. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/__init__.py +0 -0
  28. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/definition.py +0 -0
  29. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/visualizer.py +0 -0
  30. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/__init__.py +0 -0
  31. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/definition.py +0 -0
  32. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/editor.py +0 -0
  33. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/exceptions.py +0 -0
  34. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/impl.py +0 -0
  35. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  36. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/config.py +0 -0
  37. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  38. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  39. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  40. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  41. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/history.py +0 -0
  42. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  43. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/__init__.py +0 -0
  44. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  45. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/definition.py +0 -0
  46. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/impl.py +0 -0
  47. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/file_change.py +0 -0
  48. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  49. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  50. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  51. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  52. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  53. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  54. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  55. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  56. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  57. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/__init__.py +0 -0
  58. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/definition.py +0 -0
  59. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/impl.py +0 -0
  60. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/__init__.py +0 -0
  61. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/definition.py +0 -0
  62. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/impl.py +0 -0
  63. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  64. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  65. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  66. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/__init__.py +0 -0
  67. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/default.py +0 -0
  68. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/gemini.py +0 -0
  69. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/gpt5.py +0 -0
  70. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/planning.py +0 -0
  71. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  72. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  73. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/default.md +0 -0
  74. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  75. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/task_outcome.py +0 -0
  76. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/py.typed +0 -0
  77. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/__init__.py +0 -0
  78. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/impl.py +0 -0
  79. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/__init__.py +0 -0
  80. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/__init__.py +0 -0
  81. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/definition.py +0 -0
  82. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/descriptions.py +0 -0
  83. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/env.py +0 -0
  84. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/impl.py +0 -0
  85. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/metadata.py +0 -0
  86. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  87. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  88. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  89. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  90. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
  91. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  92. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  93. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/command.py +0 -0
  94. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  95. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/__init__.py +0 -0
  96. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/definition.py +0 -0
  97. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/executor.py +0 -0
  98. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/utils/__init__.py +0 -0
  99. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/utils/timeout.py +0 -0
  100. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/__init__.py +0 -0
  101. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/definition.py +0 -0
  102. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/impl.py +0 -0
  103. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  104. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  105. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/top_level.txt +0 -0
  106. {openhands_tools-1.54.0 → openhands_tools-1.55.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.54.0
3
+ Version: 1.55.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -12,6 +12,7 @@ Requires-Dist: tree-sitter>=0.24
12
12
  Requires-Dist: tree-sitter-bash>=0.23
13
13
  Requires-Dist: binaryornot>=0.6.0
14
14
  Requires-Dist: cachetools
15
+ Requires-Dist: filelock>=3.20.1
15
16
  Requires-Dist: libtmux>=0.53.0
16
17
  Requires-Dist: pydantic>=2.13.5
17
18
  Requires-Dist: browser-use>=0.8.0
@@ -13,6 +13,7 @@ from openhands.sdk.conversation.state import (
13
13
  )
14
14
  from openhands.sdk.logger import get_logger
15
15
  from openhands.sdk.subagent import get_agent_factory
16
+ from openhands.sdk.subagent.scope import inherit_mcp_config
16
17
  from openhands.sdk.tool.tool import ToolExecutor
17
18
  from openhands.tools.delegate.definition import DelegateObservation
18
19
 
@@ -188,7 +189,9 @@ class DelegateExecutor(ToolExecutor):
188
189
  worker_agent = factory.factory_func(sub_agent_llm)
189
190
 
190
191
  # ensuring that the sub-agent LLM has stream deactivated
191
- worker_agent = worker_agent.model_copy(
192
+ worker_agent = inherit_mcp_config(
193
+ parent_conversation.agent, worker_agent
194
+ ).model_copy(
192
195
  update={
193
196
  "llm": worker_agent.llm.model_copy(update={"stream": False})
194
197
  }
@@ -223,6 +226,8 @@ class DelegateExecutor(ToolExecutor):
223
226
  "_parent_llm_call_context": (
224
227
  parent_conversation.get_llm_call_context()
225
228
  ),
229
+ "mcp_tool_provider": parent_conversation._mcp_tool_provider,
230
+ "_mcp_config_from_parent": True,
226
231
  }
227
232
 
228
233
  if factory.definition.max_iteration_per_run is not None:
@@ -244,8 +244,9 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
244
244
  confirmation policy requires user approval. Receives
245
245
  `(task_id, pending_actions)` and must return `True` to
246
246
  approve or `False` to reject.
247
- sub_agent_scope: Which of this agent's capabilities the sub-agents
248
- it offers and starts must stay within.
247
+ sub_agent_scope: Whether the sub-agents it offers and starts must
248
+ stay within this agent's tools. Sub-agents always inherit this
249
+ agent's MCP servers.
249
250
 
250
251
  Returns:
251
252
  List containing a single TaskTool.
@@ -264,8 +265,7 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
264
265
  agent_types_info = get_factory_info(lambda d: d.name in registered)
265
266
  else:
266
267
  agent_types_info = (
267
- "- None: every registered agent uses tools or MCP servers "
268
- "this agent does not have."
268
+ "- None: every registered agent uses tools this agent does not have."
269
269
  )
270
270
 
271
271
  task_tool_examples = "\n".join(
@@ -35,7 +35,11 @@ from openhands.sdk.logger import get_logger
35
35
  from openhands.sdk.observability.laminar import detached_delegate_context
36
36
  from openhands.sdk.security import ConfirmationPolicyBase
37
37
  from openhands.sdk.subagent.registry import AgentFactory, get_agent_factory
38
- from openhands.sdk.subagent.scope import SubAgentScope, scope_delegation_tools
38
+ from openhands.sdk.subagent.scope import (
39
+ SubAgentScope,
40
+ inherit_mcp_config,
41
+ scope_delegation_tools,
42
+ )
39
43
 
40
44
 
41
45
  if TYPE_CHECKING:
@@ -225,6 +229,8 @@ class TaskManager:
225
229
  _parent_llm_call_context=(
226
230
  self.parent_conversation.get_llm_call_context()
227
231
  ),
232
+ mcp_tool_provider=self.parent_conversation._mcp_tool_provider,
233
+ _mcp_config_from_parent=True,
228
234
  delete_on_close=True,
229
235
  observability_metadata=self._delegate_observability_metadata(
230
236
  task_id=resume, subagent_type=subagent_type, link=link
@@ -328,6 +334,8 @@ class TaskManager:
328
334
  max_budget_per_run=max_budget_per_run,
329
335
  hook_config=hook_config,
330
336
  _parent_llm_call_context=parent.get_llm_call_context(),
337
+ mcp_tool_provider=parent._mcp_tool_provider,
338
+ _mcp_config_from_parent=True,
331
339
  delete_on_close=True,
332
340
  prompt_cache_key=str(parent.state.id),
333
341
  observability_metadata=self._delegate_observability_metadata(
@@ -391,7 +399,7 @@ class TaskManager:
391
399
  )
392
400
 
393
401
  # ensuring that the sub-agent LLM has stream deactivated
394
- sub_agent = sub_agent.model_copy(
402
+ sub_agent = inherit_mcp_config(parent.agent, sub_agent).model_copy(
395
403
  update={
396
404
  "llm": sub_agent.llm.model_copy(update={"stream": False}),
397
405
  "tools": scope_delegation_tools(sub_agent.tools, self._sub_agent_scope),
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  from collections.abc import Sequence
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, ClassVar, Literal
4
+ from typing import TYPE_CHECKING, ClassVar, Literal, get_args
5
5
 
6
- from pydantic import BaseModel, Field, ValidationError
6
+ from pydantic import BaseModel, Field, ValidationError, field_validator
7
7
 
8
8
 
9
9
  if TYPE_CHECKING:
@@ -28,6 +28,14 @@ logger = get_logger(__name__)
28
28
  # Type alias for task tracker status
29
29
  TaskTrackerStatusType = Literal["todo", "in_progress", "done"]
30
30
 
31
+ # Models trained on other agents' todo tools send these names for our statuses.
32
+ _STATUS_ALIASES: dict[str, TaskTrackerStatusType] = {
33
+ "pending": "todo",
34
+ "to_do": "todo",
35
+ "completed": "done",
36
+ "complete": "done",
37
+ }
38
+
31
39
 
32
40
  class TaskItem(BaseModel):
33
41
  title: str = Field(..., description="A brief title for the task.")
@@ -38,6 +46,16 @@ class TaskItem(BaseModel):
38
46
  "One of 'todo', 'in_progress', or 'done'.",
39
47
  )
40
48
 
49
+ @field_validator("status", mode="before")
50
+ @classmethod
51
+ def _normalize_status(cls, value: object) -> object:
52
+ if not isinstance(value, str):
53
+ return value
54
+ normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
55
+ if normalized in get_args(TaskTrackerStatusType):
56
+ return normalized
57
+ return _STATUS_ALIASES.get(normalized, value)
58
+
41
59
 
42
60
  class TaskTrackerAction(Action):
43
61
  """An action where the agent writes or updates a task list for task management."""
@@ -406,6 +424,7 @@ class TaskTrackerTool(ToolDefinition[TaskTrackerAction, TaskTrackerObservation])
406
424
  catalog_description: ClassVar[str] = (
407
425
  "Keep a running task list to organise multi-step work."
408
426
  )
427
+ conversation_local: ClassVar[bool] = True
409
428
 
410
429
  @classmethod
411
430
  def create(cls, conv_state: "ConversationState") -> Sequence["TaskTrackerTool"]:
@@ -30,12 +30,26 @@ TIMEOUT_MESSAGE_TEMPLATE: Final[str] = (
30
30
  # How long to wait with no new output before considering it a no-change timeout
31
31
  NO_CHANGE_TIMEOUT_SECONDS: Final[int] = 30
32
32
 
33
- # How often to poll for new output in seconds
33
+ # How often to poll for new output in seconds, once a command has run for a while
34
34
  POLL_INTERVAL: Final[float] = 0.5
35
+ # Shortest poll interval, used right after a command is sent
36
+ MIN_POLL_INTERVAL: Final[float] = 0.02
35
37
  HISTORY_LIMIT: Final[int] = 10_000
36
38
 
37
39
  TMUX_SOCKET_NAME: Final[str] = "openhands"
38
40
 
41
+ # tmux user option recording the PID of the process that created a socket.
42
+ # Startup cleanup reads it so it does not kill the live sessions of a sibling
43
+ # agent-server that shares one externally supplied TMUX_TMPDIR.
44
+ TMUX_SOCKET_OWNER_OPTION: Final[str] = "@openhands_socket_owner_pid"
45
+
46
+ # Cross-process lock file, inside the tmux socket directory, that serializes
47
+ # socket creation with the agent-server's startup cleanup. A socket only exists
48
+ # between creation and the recording of its owner, so both operations take this
49
+ # lock to keep cleanup from reclaiming a socket that a live process is still
50
+ # setting up.
51
+ TMUX_SOCKET_CREATION_LOCK: Final[str] = ".openhands-socket-creation.lock"
52
+
39
53
  # Tmux session dimensions (columns x rows). Keep the viewport wide enough for
40
54
  # common command output while leaving scrollback retention to HISTORY_LIMIT.
41
55
  # Output wider than TMUX_SESSION_WIDTH columns will wrap; this is an accepted
@@ -10,6 +10,7 @@ from openhands.sdk.utils import maybe_truncate
10
10
  from openhands.tools.terminal.constants import (
11
11
  CMD_OUTPUT_PS1_END,
12
12
  MAX_CMD_OUTPUT_SIZE,
13
+ MIN_POLL_INTERVAL,
13
14
  NO_CHANGE_TIMEOUT_SECONDS,
14
15
  POLL_INTERVAL,
15
16
  TIMEOUT_MESSAGE_TEMPLATE,
@@ -769,5 +770,6 @@ class TerminalSession(TerminalSessionBase):
769
770
  )
770
771
  return obs
771
772
 
772
- # Sleep before next check
773
- time.sleep(POLL_INTERVAL)
773
+ # Poll in proportion to the command's run time so a short command is
774
+ # noticed within milliseconds, not at the next POLL_INTERVAL tick.
775
+ time.sleep(min(POLL_INTERVAL, max(MIN_POLL_INTERVAL, elapsed_time / 4)))
@@ -22,13 +22,15 @@ from openhands.tools.terminal.constants import (
22
22
  HISTORY_LIMIT,
23
23
  TMUX_SESSION_HEIGHT,
24
24
  TMUX_SESSION_WIDTH,
25
- TMUX_SOCKET_NAME,
26
25
  )
27
26
  from openhands.tools.terminal.env import (
28
27
  build_terminal_env,
29
28
  normalize_terminal_env,
30
29
  )
31
- from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
30
+ from openhands.tools.terminal.terminal.tmux_terminal import (
31
+ TmuxTerminal,
32
+ _new_owned_session,
33
+ )
32
34
 
33
35
 
34
36
  logger = get_logger(__name__)
@@ -118,15 +120,12 @@ class TmuxPanePool:
118
120
  return
119
121
 
120
122
  env = build_terminal_env(self.env)
121
- # Stale handles must not reach another pool after tmux restarts.
122
- self._server = libtmux.Server(
123
- socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
124
- )
125
123
  session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
126
- self._session = self._server.new_session(
124
+ # Stale handles must not reach another pool after tmux restarts.
125
+ self._server, self._session = _new_owned_session(
126
+ env,
127
127
  session_name=session_name,
128
128
  start_directory=self.work_dir,
129
- kill_session=True,
130
129
  x=TMUX_SESSION_WIDTH,
131
130
  y=TMUX_SESSION_HEIGHT,
132
131
  )
@@ -1,11 +1,15 @@
1
1
  """Tmux-based terminal backend implementation."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  import time
5
6
  import uuid
6
- from collections.abc import Mapping
7
+ from collections.abc import Iterator, Mapping
8
+ from contextlib import contextmanager, suppress
9
+ from pathlib import Path
7
10
 
8
11
  import libtmux
12
+ from filelock import FileLock
9
13
 
10
14
  from openhands.sdk.logger import get_logger
11
15
  from openhands.sdk.utils.redact import redact_api_key_literals
@@ -13,7 +17,9 @@ from openhands.tools.terminal.constants import (
13
17
  HISTORY_LIMIT,
14
18
  TMUX_SESSION_HEIGHT,
15
19
  TMUX_SESSION_WIDTH,
20
+ TMUX_SOCKET_CREATION_LOCK,
16
21
  TMUX_SOCKET_NAME,
22
+ TMUX_SOCKET_OWNER_OPTION,
17
23
  )
18
24
  from openhands.tools.terminal.env import (
19
25
  build_terminal_env,
@@ -68,6 +74,71 @@ def _install_libtmux_redaction_filter() -> None:
68
74
  libtmux_logger.addFilter(_SecretRedactFilter())
69
75
 
70
76
 
77
+ @contextmanager
78
+ def _socket_creation_lock() -> Iterator[None]:
79
+ """Serialize socket creation with the agent-server's startup cleanup.
80
+
81
+ A tmux socket exists between ``new-session`` and the recording of its owner
82
+ option. Startup cleanup in a sibling agent-server treats an ownerless socket
83
+ as reclaimable, so without this lock it can kill a socket whose live owner
84
+ has not finished initializing. The lock file lives in the tmux socket
85
+ directory, which is the one path every process sharing ``TMUX_TMPDIR``
86
+ agrees on even while tmux has not created any socket yet.
87
+ """
88
+ socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
89
+ # tmux refuses a socket directory that is group/world accessible, and
90
+ # FileLock would create it with the umask default, so create it 0700 first.
91
+ socket_dir.mkdir(parents=True, exist_ok=True)
92
+ with suppress(OSError):
93
+ socket_dir.chmod(0o700)
94
+ with FileLock(socket_dir / TMUX_SOCKET_CREATION_LOCK):
95
+ yield
96
+
97
+
98
+ def mark_socket_owner(server: libtmux.Server) -> None:
99
+ """Record this process as the owner of *server*'s socket.
100
+
101
+ The agent-server's startup cleanup reads this to tell a live sibling's
102
+ socket apart from a stale one when a shared ``TMUX_TMPDIR`` is in use.
103
+ Best-effort: a failure here only loses the ownership hint, so it must
104
+ never break terminal setup.
105
+ """
106
+ with suppress(Exception):
107
+ server.set_option(
108
+ TMUX_SOCKET_OWNER_OPTION, str(os.getpid()), global_=True, scope=None
109
+ )
110
+
111
+
112
+ def _new_owned_session(
113
+ env: Mapping[str, str],
114
+ *,
115
+ session_name: str,
116
+ start_directory: str,
117
+ x: int,
118
+ y: int,
119
+ ) -> tuple[libtmux.Server, libtmux.Session]:
120
+ """Create a tmux session on a fresh socket owned by this process.
121
+
122
+ The socket only starts to exist once the session is created, so the session
123
+ is created and its owner recorded while holding the cross-process creation
124
+ lock. Startup cleanup in another process takes the same lock, so it can
125
+ never observe the socket before its owner is published.
126
+ """
127
+ with _socket_creation_lock():
128
+ server = libtmux.Server(
129
+ socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=dict(env)
130
+ )
131
+ session = server.new_session(
132
+ session_name=session_name,
133
+ start_directory=start_directory,
134
+ kill_session=True,
135
+ x=x,
136
+ y=y,
137
+ )
138
+ mark_socket_owner(server)
139
+ return server, session
140
+
141
+
71
142
  # Map normalized special key names to tmux key names.
72
143
  _TMUX_SPECIALS: dict[str, str] = {
73
144
  "ENTER": "Enter",
@@ -122,10 +193,6 @@ class TmuxTerminal(TerminalInterface):
122
193
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
123
194
  env.setdefault("GIT_PAGER", "cat")
124
195
  env.setdefault("PAGER", "cat")
125
- # Never reuse a socket: tmux numeric IDs restart at zero after server exit.
126
- self.server = libtmux.Server(
127
- socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
128
- )
129
196
  _shell_command = "/bin/bash"
130
197
  if self.username in ["root", "openhands"]:
131
198
  # This starts a non-login (new) shell for the given user
@@ -134,11 +201,12 @@ class TmuxTerminal(TerminalInterface):
134
201
  window_command = _shell_command
135
202
 
136
203
  logger.debug(f"Initializing tmux terminal with command: {window_command}")
204
+ # Never reuse a socket: tmux numeric IDs restart at zero after server exit.
137
205
  session_name = f"openhands-{self.username}-{uuid.uuid4()}"
138
- self.session = self.server.new_session(
206
+ self.server, self.session = _new_owned_session(
207
+ env,
139
208
  session_name=session_name,
140
209
  start_directory=self.work_dir,
141
- kill_session=True,
142
210
  x=TMUX_SESSION_WIDTH,
143
211
  y=TMUX_SESSION_HEIGHT,
144
212
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.54.0
3
+ Version: 1.55.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -12,6 +12,7 @@ Requires-Dist: tree-sitter>=0.24
12
12
  Requires-Dist: tree-sitter-bash>=0.23
13
13
  Requires-Dist: binaryornot>=0.6.0
14
14
  Requires-Dist: cachetools
15
+ Requires-Dist: filelock>=3.20.1
15
16
  Requires-Dist: libtmux>=0.53.0
16
17
  Requires-Dist: pydantic>=2.13.5
17
18
  Requires-Dist: browser-use>=0.8.0
@@ -3,6 +3,7 @@ tree-sitter>=0.24
3
3
  tree-sitter-bash>=0.23
4
4
  binaryornot>=0.6.0
5
5
  cachetools
6
+ filelock>=3.20.1
6
7
  libtmux>=0.53.0
7
8
  pydantic>=2.13.5
8
9
  browser-use>=0.8.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.54.0"
3
+ version = "1.55.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "tree-sitter-bash>=0.23",
11
11
  "binaryornot>=0.6.0",
12
12
  "cachetools",
13
+ "filelock>=3.20.1",
13
14
  "libtmux>=0.53.0",
14
15
  "pydantic>=2.13.5",
15
16
  "browser-use>=0.8.0",