Even with dot-sourcing (.), a script cannot instruct its caller to exit, so in your current setup you need some conditional in main.ps1 that checks for the need to exit after the . .\inner.ps1 call.
Given that helper script inner.ps1 is being dot-sourced and can therefore modify the environment of the caller, main.ps1, one wonders why recursion is necessary altogether.
The remainder of this answer accepts the premise of the question, however.
The OP himself came up with a solution that encapsulates the recursive-invocation details inside inner.ps1, but the problem is that using the success stream (the regular output stream) to send control information - $TRUE or $FALSE - means that the if statement that checks that information (if (. .\inner.ps1) ...) consumes the regular output from main1.ps1 and therefore suppresses it.[1]
In other words: the approach only works if main.ps1 happens to produce no output at all, or the caller happens not to care what main.ps1 outputs.
The solution is to base the control flow on exit codes (which PowerShell reflects in automatic variable $LASTEXITCODE):
### inner.ps1
if(-not $__inner_already_recursed) {
'inner: reinvoking main...'
$__inner_already_recursed=$true # set flag to indicate that recursion has happened
. $myInvocation.ScriptName # re-invoke main.ps1
exit 0 # Signal need to continue execution with $LASTEXITCODE 0
} else {
'inner: not reinvoking...'
exit 1 # Signal that this is already the 2nd invocation.
}
and:
### main.ps1
'main: entering'
# Call the helper script that performs the recursive invocation.
. .\inner.ps1
if ($LASTEXITCODE -eq 1) { exit } # 1 means previously already reinvoked -> exit
# We only get here once, in the recursive invocation.
'main: should only get here once'
As the OP states, the auxiliary variable used to maintain the recursive-invocation state in inner.ps1 must not preexist (or at least not with a truthy value), so its name - $__inner_already_recursed - was chosen to minimize that risk.
[1] Write-Host calls aren't affected, because they print directly to the console, but their output can neither be captured nor sent through the pipeline.
(With additional effort, you could in PSv5+, but that's generally ill-advised and not helpful here.)