refactor(app): make session start terminal

Move live-session startup and its existing diagnostics into the typed Phase-9 owner. GameWindow.OnLoad now ends immediately after that phase, so no callback binding, publication, or allocation can occur after the session becomes active.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 19:06:00 +02:00
parent 3628aeb520
commit 54244d31f1
11 changed files with 139 additions and 39 deletions

View file

@ -35,9 +35,10 @@ What does NOT go here:
every late edge explicitly. I.8a now owns the focused session factory, every late edge explicitly. I.8a now owns the focused session factory,
command targets, and sole gameplay input subscriber before frame publication. command targets, and sole gameplay input subscriber before frame publication.
I.8b now owns the complete update/render construction, exact atomic frame I.8b now owns the complete update/render construction, exact atomic frame
publication, and lifecycle resource snapshot source. `GameWindow.OnUpdate` publication, and lifecycle resource snapshot source. I.8c makes session
and `GameWindow.OnRender` are typed orchestration handoffs and the class is start the terminal `OnLoad` operation. `GameWindow.OnUpdate` and
1,919 raw lines. I.8cI.9 plus the remaining `GameWindow.OnRender` are typed orchestration handoffs and the class is
1,910 raw lines. I.9 plus the remaining
shutdown/closeout shutdown/closeout
cleanup is active in cleanup is active in
[`docs/architecture/code-structure.md`](architecture/code-structure.md). [`docs/architecture/code-structure.md`](architecture/code-structure.md).

View file

@ -739,10 +739,11 @@ diagnostic command targets, and sole gameplay input subscriber into Phase 7
before frame publication. I.8b moves the complete update/render construction before frame publication. I.8b moves the complete update/render construction
body into `FrameRootCompositionPhase`, publishes the pair through an exact body into `FrameRootCompositionPhase`, publishes the pair through an exact
lease, and gives lifecycle resource sampling a focused source. `GameWindow` is lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
1,919 raw lines. I.8cL remain active, with terminal session start next. The 1,910 raw lines after I.8c makes live-session start the terminal `OnLoad`
App gate passes 3,429 tests / 3 intentional skips and the complete Release suite operation. I.9L remain active, with the complete Checkpoint-I audit next. The
passes 7,801 tests / 5 intentional skips. The clean solution build retains only App gate passes 3,430 tests / 3 intentional skips and the complete Release suite
the 17 test-project warnings tracked by #228; the I.8b corrected-diff review is passes 7,802 tests / 5 intentional skips. The clean solution build retains only
the 17 test-project warnings tracked by #228; the I.8c corrected-diff review is
clean. clean.
### 4.4 Exit criteria ### 4.4 Exit criteria

View file

@ -104,11 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
resources have transactional phase ownership and exact-owner late bindings. resources have transactional phase ownership and exact-owner late bindings.
I.7 adds the complete streaming/session/hydration/local-player/teleport phase, I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
including explicit reversible ownership for every late edge. `GameWindow` is including explicit reversible ownership for every late edge. `GameWindow` is
now 1,919 raw lines after I.8b, which also composes and atomically publishes the now 1,910 raw lines after I.8c, which makes session start the terminal `OnLoad`
complete update/render pair and extracts lifecycle resource sampling. I.8cI.9, operation. I.9, shutdown, canonical soak snapshots, and closeout checkpoints
shutdown, canonical soak snapshots, and closeout checkpoints JL remain. The JL remain. The App gate passes 3,430 tests / 3 intentional skips, the complete
App gate passes 3,429 tests / 3 intentional skips, the complete Release suite Release suite passes 7,802 tests / 5 skips, and the I.8c corrected-diff review
passes 7,801 tests / 5 skips, and the I.8b corrected-diff review is clean. is clean.
This is a behavior-preserving structural program. Severe regressions still get This is a behavior-preserving structural program. Severe regressions still get
root-cause fixes in separate commits; ordinary feature work resumes with M4 root-cause fixes in separate commits; ordinary feature work resumes with M4

View file

@ -62,11 +62,12 @@ session, hydration, local-player, combat, and teleport startup graph, including
named reversible late edges. I.8a adds the focused live-session runtime factory named reversible late edges. I.8a adds the focused live-session runtime factory
and attaches command/input owners before frame publication. I.8b composes and and attaches command/input owners before frame publication. I.8b composes and
atomically publishes the complete frame pair and extracts lifecycle resource atomically publishes the complete frame pair and extracts lifecycle resource
sampling. `GameWindow` is 1,919 raw lines. I.8cI.9 and checkpoints JL remain sampling. I.8c makes session start the terminal `OnLoad` operation.
active. Issue #232 tracks `GameWindow` is 1,910 raw lines. I.9 and checkpoints JL remain active. Issue
#232 tracks
process-residency variance in the soak without loosening its leak threshold. process-residency variance in the soak without loosening its leak threshold.
The App gate passes 3,429 tests / 3 skips, the complete Release suite passes The App gate passes 3,430 tests / 3 skips, the complete Release suite passes
7,801 tests / 5 skips, and the I.8b corrected-diff review is clean. 7,802 tests / 5 skips, and the I.8c corrected-diff review is clean.
Carried: Carried:
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus

View file

@ -297,8 +297,10 @@ ledger only after the complete pipeline is live.
input subscriber now belong to Phase 7 and publish before any frame root. input subscriber now belong to Phase 7 and publish before any frame root.
I.8b composes the unchanged update/render graph in a focused phase, publishes I.8b composes the unchanged update/render graph in a focused phase, publishes
the pair through an exact owned slot lease, and moves lifecycle resource the pair through an exact owned slot lease, and moves lifecycle resource
sampling out of the window. `GameWindow.cs` is 1,919 raw lines. I.8c terminal sampling out of the window. I.8c makes the exact session start and its
session start is the active cut. existing diagnostics the terminal `OnLoad` operation. `GameWindow.cs` is
1,910 raw lines. I.9 corrected-diff review and documentation closeout is the
active cut.
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline. Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
## 6. Automated acceptance ## 6. Automated acceptance

View file

@ -1,6 +1,6 @@
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start # GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
**Status:** Active — I.8aI.8b complete; I.8c terminal start next **Status:** Complete 2026-07-22
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md` **Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
@ -144,6 +144,12 @@ shutdown. Replacement is rejected atomically.
## 4. I.8c — terminal start and window cutover ## 4. I.8c — terminal start and window cutover
**Completed 2026-07-22.** `SessionStartCompositionPhase` invokes the exact
Phase-7 host through the Phase-8 result and owns the existing missing-credential
and failed-start diagnostics. The phase call is the final statement in
`GameWindow.OnLoad`; no binding, publication, allocation, or diagnostics setup
follows it.
Add `SessionStartCompositionPhase` implementing Add `SessionStartCompositionPhase` implementing
`ISessionStartCompositionPhase<FrameRootResult>`. It invokes only `ISessionStartCompositionPhase<FrameRootResult>`. It invokes only
`LiveSessionHost.Start(RuntimeOptions)` and preserves the existing diagnostics `LiveSessionHost.Start(RuntimeOptions)` and preserves the existing diagnostics

View file

@ -29,8 +29,8 @@ audit.
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
explicit single ownership and transfer seams. explicit single ownership and transfer seams.
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. I.1I.8b are complete; I.8c transactional partial-acquisition rollback. I.1I.8 are complete; I.9
terminal session start is active. Detailed plan: corrected-diff review and checkpoint closeout is active. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md). [`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime - [ ] J — move the exact retryable shutdown manifest to a focused lifetime
owner and prove all partial-load/reentrant/retry paths. owner and prove all partial-load/reentrant/retry paths.

View file

@ -0,0 +1,47 @@
using AcDream.App.Net;
namespace AcDream.App.Composition;
internal sealed record SessionStartDependencies(
RuntimeOptions Options,
Action<string> Log);
/// <summary>
/// Terminal startup phase. Every callback, command target, and frame root is
/// already published when this owner asks the canonical session host to start.
/// </summary>
internal sealed class SessionStartCompositionPhase
: ISessionStartCompositionPhase<FrameRootResult>
{
private readonly SessionStartDependencies _dependencies;
public SessionStartCompositionPhase(SessionStartDependencies dependencies) =>
_dependencies = dependencies
?? throw new ArgumentNullException(nameof(dependencies));
public void Start(FrameRootResult frame)
{
ArgumentNullException.ThrowIfNull(frame);
LiveSessionStartResult result =
frame.SessionHost.Start(_dependencies.Options);
Report(result, _dependencies.Log);
}
internal static void Report(
LiveSessionStartResult result,
Action<string> log)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(log);
switch (result.Status)
{
case LiveSessionStartStatus.MissingCredentials:
log(
"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping");
break;
case LiveSessionStartStatus.Failed:
log($"live: session failed: {result.Error}");
break;
}
}
}

View file

@ -1420,18 +1420,9 @@ public sealed class GameWindow :
livePresentation, livePresentation,
sessionPlayer); sessionPlayer);
AcDream.App.Net.LiveSessionStartResult liveStart = new SessionStartCompositionPhase(
frameRoots.SessionHost.Start(_options); new SessionStartDependencies(_options, Console.WriteLine))
switch (liveStart.Status) .Start(frameRoots);
{
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
Console.WriteLine(
"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping");
break;
case AcDream.App.Net.LiveSessionStartStatus.Failed:
Console.WriteLine($"live: session failed: {liveStart.Error}");
break;
}
} }
private void OnUpdate(double dt) private void OnUpdate(double dt)

View file

@ -0,0 +1,38 @@
using AcDream.App.Composition;
using AcDream.App.Net;
namespace AcDream.App.Tests.Composition;
public sealed class SessionStartCompositionTests
{
[Fact]
public void DiagnosticsPreserveMissingCredentialAndFailureMessagesOnly()
{
var messages = new List<string>();
SessionStartCompositionPhase.Report(
new LiveSessionStartResult(
LiveSessionStartStatus.MissingCredentials),
messages.Add);
SessionStartCompositionPhase.Report(
new LiveSessionStartResult(
LiveSessionStartStatus.Failed,
Error: new InvalidOperationException("boom")),
messages.Add);
SessionStartCompositionPhase.Report(
new LiveSessionStartResult(LiveSessionStartStatus.Disabled),
messages.Add);
SessionStartCompositionPhase.Report(
new LiveSessionStartResult(LiveSessionStartStatus.Connected),
messages.Add);
Assert.Equal(2, messages.Count);
Assert.Equal(
"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping",
messages[0]);
Assert.StartsWith(
"live: session failed: System.InvalidOperationException: boom",
messages[1],
StringComparison.Ordinal);
}
}

View file

@ -66,7 +66,8 @@ public sealed class GameWindowSlice8BoundaryTests
"this).Compose(", "this).Compose(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"FrameRootResult frameRoots = new FrameRootCompositionPhase(", "FrameRootResult frameRoots = new FrameRootCompositionPhase(",
"frameRoots.SessionHost.Start(_options)"); "new SessionStartCompositionPhase(",
".Start(frameRoots);");
string framePhase = File.ReadAllText(Path.Combine( string framePhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -97,7 +98,7 @@ public sealed class GameWindowSlice8BoundaryTests
"_publication.PublishSessionPlayer(result);"); "_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences( Assert.Equal(1, CountOccurrences(
body, body,
"frameRoots.SessionHost.Start(_options)")); ".Start(frameRoots);"));
string phaseOne = Slice( string phaseOne = Slice(
File.ReadAllText(Path.Combine( File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -117,11 +118,22 @@ public sealed class GameWindowSlice8BoundaryTests
"if (firstKeyboard is not null && firstMouse is not null)", "if (firstKeyboard is not null && firstMouse is not null)",
phaseOne, phaseOne,
StringComparison.Ordinal); StringComparison.Ordinal);
string startPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionStartComposition.cs"));
AssertAppearsInOrder(
startPhase,
"frame.SessionHost.Start(_dependencies.Options)",
"switch (result.Status)",
"case LiveSessionStartStatus.MissingCredentials:",
"case LiveSessionStartStatus.Failed:");
int start = body.IndexOf( int start = body.IndexOf(
"frameRoots.SessionHost.Start(_options)", ".Start(frameRoots);",
StringComparison.Ordinal); StringComparison.Ordinal);
string postStart = body[start..]; string postStart = body[(start + ".Start(frameRoots);".Length)..];
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
Assert.Empty(System.Text.RegularExpressions.Regex.Matches( Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
postStart, postStart,
@"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)")); @"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)"));
@ -343,7 +355,8 @@ public sealed class GameWindowSlice8BoundaryTests
"new WorldRenderCompositionPhase(", "new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(", "new FrameRootCompositionPhase(",
"frameRoots.SessionHost.Start(_options)"); "new SessionStartCompositionPhase(",
".Start(frameRoots);");
string sessionPhase = File.ReadAllText(Path.Combine( string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",