The current semantics is that child tasks do not "inherit" the actor in which they were created. Rather, they go onto the global executor so they can execute concurrently with the body that created them (here, it's doStuff). This is the default because the point of async let is to introduce concurrency.
One important thing your example is missing is the declaration of doOtherStuff(). If it's also part of the @MainActor, e.g., you have code like this:
@MainActor func doOtherStuff() { ... }
then that code will end up running on the main actor anyway. If it's not part of the main actor:
func doOtherStuff() { ... } // not part of an actor at all!
then it runs concurrently.
One important thing to internalize about the actor model is that a declaration knows what actor it needs to run on, and Swift makes sure that happens. The place where you initiate a call to the declaration is far less important.
async let doesn't inherit actor context because doing so would introduce unnecessarily serialization on the actor, and the actor model already ensures that you'll come back to the actor when needed.
Here's another instance of this same question/sentiment:
async let is the deliberate choice to introduce concurrency. Immediately eliminating that concurrency by having the child task go back to the main actor---even if you don't need it---would undo the benefits. We'll require a hop back to the main actor when you use anything that needs to be on the main actor.
FWIW, for (instance) actors this feature was part of the SE-0313 review, but has been subsetted out of that proposal due to concerns about how the feature behaved. It could come back later, of course.
Doug