diff --git a/claude_bottle/backend/smolmachines/bottle.py b/claude_bottle/backend/smolmachines/bottle.py index f42d066..d2eb01b 100644 --- a/claude_bottle/backend/smolmachines/bottle.py +++ b/claude_bottle/backend/smolmachines/bottle.py @@ -22,9 +22,21 @@ import sys from typing import Mapping from .. import Bottle, ExecResult +from . import pty_resize as _pty_resize from . import smolvm as _smolvm +# Absolute path to the pty_resize wrapper. The dashboard's tmux +# pane (split-window / respawn-pane) opens the new pane in its +# OWN cwd, not the cwd of the process running split-window — so +# invoking the wrapper as `python -m ` would fail +# with ModuleNotFoundError whenever the operator's tmux pane was +# started from anywhere outside the claude-bottle repo. Absolute +# path sidesteps the cwd dependence (the wrapper has no +# claude_bottle.* imports, so it runs as a standalone script). +_PTY_RESIZE_SCRIPT = _pty_resize.__file__ + + # Per-user env the agent image's USER (node) expects. claude # reads ~/.claude.json + writes session state under ~/.claude/; # bare `runuser -u` inherits root's HOME=/root, which claude @@ -96,8 +108,7 @@ class SmolmachinesBottle(Bottle): # happen to go through this method) stay light. return flags return [ - sys.executable, "-m", - "claude_bottle.backend.smolmachines.pty_resize", + sys.executable, _PTY_RESIZE_SCRIPT, self.name, "--", *flags, ] diff --git a/tests/unit/test_smolmachines_bottle.py b/tests/unit/test_smolmachines_bottle.py index a6d5777..58d3039 100644 --- a/tests/unit/test_smolmachines_bottle.py +++ b/tests/unit/test_smolmachines_bottle.py @@ -16,6 +16,7 @@ from __future__ import annotations import sys import unittest +from claude_bottle.backend.smolmachines import pty_resize as _pty_resize from claude_bottle.backend.smolmachines.bottle import SmolmachinesBottle @@ -40,13 +41,15 @@ class TestClaudeArgvWrapped(unittest.TestCase): def test_pty_resize_wrapper_prefix(self): argv = _bottle().claude_argv([]) + # Absolute script path (not `-m `) so the tmux + # pane's cwd doesn't matter — see the `_PTY_RESIZE_SCRIPT` + # docstring in bottle.py. self.assertEqual( [ - sys.executable, "-m", - "claude_bottle.backend.smolmachines.pty_resize", + sys.executable, _pty_resize.__file__, "claude-bottle-dev-abc", "--", ], - argv[:5], + argv[:4], ) def test_minimal_inner_argv_no_prompt(self): @@ -128,7 +131,7 @@ class TestClaudeArgvNoTTY(unittest.TestCase): def test_no_wrapper_when_tty_false(self): argv = _bottle().claude_argv([], tty=False) self.assertEqual("smolvm", argv[0]) - self.assertNotIn("pty_resize", " ".join(argv)) + self.assertFalse(any("pty_resize" in a for a in argv)) def test_tty_false_drops_it_flags(self): argv = _bottle().claude_argv([], tty=False)