From 794e8666e1f7fcaf4bf76e02a4c39c87c9270c82 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 27 May 2026 20:26:42 -0400 Subject: [PATCH] fix(smolmachines): invoke pty_resize by absolute path, not python -m MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard's launch path crashed inside tmux but worked outside it. Root cause: `python -m claude_bottle.backend.smolmachines.pty_resize` needs the `claude_bottle` package on `sys.path`, which by default comes from cwd. The outside-tmux path is `subprocess.run(...)` — inherits the dashboard process's cwd (the repo root, where `claude_bottle/` lives), so the import resolves. The inside-tmux path is `tmux split-window / respawn-pane `, and tmux opens the new pane with the pane's OWN cwd, not the cwd of the process invoking split-window. If the operator started their tmux pane anywhere outside the repo (typical: `$HOME`), the wrapper hit `ModuleNotFoundError: No module named 'claude_bottle'` and tmux closed the pane immediately. Sidestep the cwd dependence by invoking the wrapper as `python ` instead of `python -m `. The wrapper has no `claude_bottle.*` imports — it's stdlib-only — so it runs as a standalone script anywhere on the filesystem. The absolute path comes from `pty_resize.__file__` at module-load time. Tests: - `test_pty_resize_wrapper_prefix`: updated to assert the absolute-script-path shape rather than the `-m ` shape. - `test_no_wrapper_when_tty_false`: the substring check now uses `any("pty_resize" in a for a in argv)` instead of string-joining (so the absolute path's "pty_resize.py" filename match still catches a regression). 636 unit tests pass. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/backend/smolmachines/bottle.py | 15 +++++++++++++-- tests/unit/test_smolmachines_bottle.py | 11 +++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) 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)