docs: Campaign LA — pinned launch-contract schema COMMITTED into plan LA1

The LA3 Opus review process note was right: the contract both sides
implement lived only in orchestrator prompts, which is exactly the drift
mode the pin exists to prevent (and it produced the paths-key CRITICAL).
The schema, field rules, probe-mode discriminator, and status vocabulary
are now a binding plan section; amendments change this text first,
implementations second. Ledger: LA3 fix round dispatched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 16:04:32 +02:00
parent 0bcc7ba3a3
commit db9ad53c1c
38 changed files with 2397 additions and 40 deletions

View file

@ -113,6 +113,19 @@ internal sealed class HeadlessSessionHost : IDisposable
private readonly HeadlessCredentialSecret _credential;
private readonly HeadlessDiagnosticWriter _diagnostics;
/// <summary>
/// Campaign LA slice LA1: a SEPARATE per-session sink from
/// <see cref="_diagnostics"/> — a no-op instance when
/// <see cref="HeadlessSessionDescriptor.StatusFile"/> was not configured.
/// See <see cref="SessionStatusWriter"/>'s own doc for why this is not a
/// rework of the shared-stdout diagnostics writer.
/// </summary>
private readonly SessionStatusWriter _statusWriter;
/// <summary>Guards <see cref="Stop"/>'s <c>disconnected</c> status event
/// so a Stop() on a session that never actually reached Connected (e.g.
/// disposing a fresh, never-started host) does not report a spurious
/// disconnect.</summary>
private bool _hasConnected;
/// <summary>
/// Campaign OP slice OP7 (2026-08-11), D8: the parsed
/// <c>characterOptions</c> block — empty when the config omitted it.
/// Parsed once at construction; <see cref="HeadlessConfigurationLoader"/>
@ -271,6 +284,10 @@ internal sealed class HeadlessSessionHost : IDisposable
var commands = new DirectGameRuntimeCommandAdapter(
runtime,
bridge);
// Campaign LA slice LA1: no-op instance when
// descriptor.StatusFile is unset — every call site below stays
// unconditional.
var statusWriter = new SessionStatusWriter(descriptor.StatusFile);
var liveSession = new LiveSessionHost(
runtime.Session,
new LiveSessionHostBindings(
@ -318,14 +335,25 @@ internal sealed class HeadlessSessionHost : IDisposable
descriptor.Id,
$"connecting:{host}:{port}:{user}",
runtime.Generation.Value),
() => diagnostics.Message(
() =>
{
diagnostics.Message(
descriptor.Id,
"connected",
runtime.Generation.Value);
statusWriter.Connected(descriptor.Id);
_hasConnected = true;
},
roster => statusWriter.CharacterList(descriptor.Id, roster),
selection => statusWriter.EnteredWorld(
descriptor.Id,
"connected",
runtime.Generation.Value)));
selection.CharacterId,
selection.CharacterName)));
Runtime = runtime;
Commands = commands;
_liveSession = liveSession;
_statusWriter = statusWriter;
_localPlayerFrame =
runtime.CreateLocalPlayerFrameController(
new HeadlessLocalPlayerFrameHost(
@ -421,8 +449,13 @@ internal sealed class HeadlessSessionHost : IDisposable
_pendingConfirmation = null;
}
internal RuntimeSessionStartResult Start() =>
Commands.Session.Start(Runtime.Generation);
internal RuntimeSessionStartResult Start()
{
// Campaign LA slice LA1: "started" = session host start — the
// earliest point this session actually attempts to connect.
_statusWriter.Started(_descriptor.Id);
return Commands.Session.Start(Runtime.Generation);
}
internal RuntimeSessionStartResult Reconnect() =>
Commands.Session.Reconnect(Runtime.Generation);
@ -470,6 +503,15 @@ internal sealed class HeadlessSessionHost : IDisposable
// (possibly disposed) WorldSession in the window between this Stop
// and the next CreateEventRoute call.
_currentSession = null;
// Campaign LA slice LA1: only report a disconnect for a session that
// actually reached Connected — a Stop() on a never-started or
// never-connected host (e.g. immediate Dispose()) is not a real
// disconnect.
if (_hasConnected)
{
_hasConnected = false;
_statusWriter.Disconnected(_descriptor.Id, "stopped");
}
return result;
}
@ -592,6 +634,13 @@ internal sealed class HeadlessSessionHost : IDisposable
_descriptor.Id,
"disposed",
_stoppedGeneration);
// Campaign LA slice LA1: "exited" = terminal — the sole
// point every disposal path (graceful and post-
// quarantine) converges on.
_statusWriter.Exited(
_descriptor.Id,
_faulted ? 1 : 0,
_faulted ? "fault" : "disposed");
_disposeStage++;
_disposed = true;
break;