This is the correct and expected behavior nowadays.
I do understand though this can be difficult to follow since we had a number of inference rules come out over time and they not have exactly intuitive interactions.
What happens here is that Task.init is the "special 'different' one", all the other APIs about child tasks do not automagically inherit through the closing over over an isolated parameter because their purpose is to introduce parallelism.
TaskGroup is designed specifically to introduce concurrency and run child tasks in parallel. If we just inferred by capture we'd constantly linearize their execution almost by accident one might say.
If you're curious on the exact mechanics:
The enqueue happens on the isolation we obtain from the @isolation(any)
closure, so closure.isolation
basically. That type is inferred currently only e.g. main actors, and we're missing "closure isolation control" to allow it for instance actors.
The "just capture the isolated param" is the Task.init special case, not the general rule.
Future directions
What you're expecting is what the following pitch would enable: Closure isolation control but we've not implemented it yet. Then you could do g.addTask { [isolated param] in }
which then would indeed enqueue and isolate to that parameter.
Closure isolation control currently is not implemented, with the exception of global actors since you can addTask { @MainActor in }
for example. In other words, this would do what you expected:
group.addTask { @MainActor in
MainActor.assertIsolated("inside addTask, @MainActor")
}
but just closing over an isolated param does not.
Hope this helps