Thank you all for the replies and sorry for the late response!
@MainActor works when using swift concurrency, not dispatch queues.
I understand that the compiler cannot reason about my code using dispatch queues. But I would expect that when leaving or entering the Swift concurrency "world" unsafely, I get warned about that fact. In particular when using Combine it is easy to forget that queue switches can happen. If this is a gap that is not (or cannot be) covered that would be a real bummer to the goal of Swift concurrency from my perspective.
The issue is that the closure parameter of sink should be inferred to be @Sendable but it is not. Thus, because it's not @Sendable, the closure is instead being granted @MainActor isolation (inherited from its context). Not sure why there isn't then a warning about losing that actor isolation when passing it to sink.
Got it. Can you tell if this is a Combine or a Swift issue? Is Swift supposed to automatically infer @Sendable in this case since it cannot ensure that it is executed in the same context again? Should Combine explicitly add the annotation?
Does it make sense to write a bug report for this or is anyone of you aware if this is intentional somehow (e.g. to avoid excessive warnings during the transition)?
Anyway. If you add @Sendable to the closure instead of @MainActor , then you'll get the diagnostics I would have expected:
Great! Happy that this matches my mental model 
Got a follow up question on this, though. The following code fails with the inline error:
.sink {@Sendable [weak self] _ in
Task {
// error: Reference to captured var 'self' in concurrently-executing code
await self?.updateState()
}
}
By unwrapping self, though, I'm able to use it in the Task:
.sink {@Sendable [weak self] _ in
guard let self else { return } // <----
Task {
await self.updateState()
}
}
That solution is fine (it avoids a retain cycle with the Combine subscription) but I'm wondering why the explicit unwrapping is needed. My naive assumption would be that Optional<Wrapped> becomes Sendable when Wrapped is. And that the @MainActor annotation implicitly makes my type Sendable (explicitly marking the type Sendable doesn't make a difference either). Is this even the problem? Do I miss something here?
The other part to that is the error message. It doesn't really help to understand the underlying issue. Do you agree?
This is basically all you need to know. The entire actor-isolation is for Swift concurrency and ensures that code isolated to a certain actor is not accessed from another (or non-isolated context)
Exactly the "(or non-isolated context)" part is happening in my example, though, isn't it?
If you want to handle the main thread hop in the sink you could use
Task { @MainActor [weak self] in
self?.updateState()
}
That's neat. I guess it achieves the same as my code above (with the guard let). With your code the whole block is executed in the context of the MainActor and with my code above only the updateState() call. Correct? I guess no benefits for one or the other?
Thanks again everyone! This is helping me ton :)