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

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using AcDream.App.Configuration;
using AcDream.App.Rendering.Residency;
using AcDream.App.Streaming;
using AcDream.Runtime.Session;
namespace AcDream.App;
@ -62,7 +65,34 @@ public sealed record RuntimeOptions(
string? VulkanDeviceOverride,
string? VulkanForcedUnsupportedFeature,
bool VulkanCapabilityProbe,
int VulkanCapabilityProbeFrames)
int VulkanCapabilityProbeFrames,
/// <summary>Campaign LA slice LA1: the raw <c>--session-config</c> path,
/// or <see langword="null"/> when the flag was not supplied (the env-var
/// dev flow). Kept for diagnostics/logging only.</summary>
string? SessionConfigPath,
/// <summary>Campaign LA slice LA1: the configured session's id, used as
/// the <c>sessionId</c> field on every status-stream event. Defaults to
/// <c>"app"</c> at every call site when unset (env-var flow).</summary>
string? SessionId,
/// <summary>Campaign LA slice LA1: the session-config character
/// selector, or <see langword="null"/> for today's existing
/// first-available fallback (absent selector = LA7's char-select screen
/// stop point once that slice lands; this slice does not build the
/// screen).</summary>
LiveSessionCharacterSelector? LiveCharacterSelector,
/// <summary>Campaign LA slice LA1: absolute path for the status-event
/// JSONL stream. <see langword="null"/> = no writer constructed.</summary>
string? StatusFilePath,
/// <summary>Campaign LA slice LA1: plugin ids to load.
/// <see langword="null"/> = load every discovered plugin (today's
/// behavior). Consumed by LA5; parsed and carried now.</summary>
IReadOnlyList<string>? Plugins,
/// <summary>Campaign LA slice LA1: ordered chat-typed strings run once
/// entered-world. Consumed by LA6; parsed and carried now.</summary>
IReadOnlyList<string> LoginCommands,
/// <summary>Campaign LA slice LA1: inter-command delay for
/// <see cref="LoginCommands"/>, milliseconds.</summary>
int LoginCommandDelayMs)
{
/// <summary>
/// Build options from the process environment. Used by
@ -170,9 +200,72 @@ public sealed record RuntimeOptions(
// closes the window. Zero -- unset, unparseable, or an explicit 0 --
// keeps the interactive behaviour, so no existing invocation changes.
VulkanCapabilityProbeFrames:
TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0);
TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0,
// Campaign LA slice LA1: the env-var dev flow never carries a
// session-config document — every new field below stays at its
// "nothing configured" default. RuntimeOptions.FromSessionConfig
// overlays the real values on top of this base.
SessionConfigPath: null,
SessionId: null,
LiveCharacterSelector: null,
StatusFilePath: null,
Plugins: null,
LoginCommands: [],
LoginCommandDelayMs: 500);
}
/// <summary>
/// Campaign LA slice LA1: builds options for the <c>--session-config</c>
/// launch path. Starts from the same env-var parse as
/// <see cref="FromEnvironment"/> (diagnostic/dev flags are still
/// env-controlled — only the LIVE session settings and the five new LA1
/// fields come from the document) and overlays the resolved session.
/// <paramref name="resolvedPassword"/> is revealed into
/// <see cref="LivePass"/> exactly as wide as the existing env-var flow —
/// see that field's own doc.
/// </summary>
internal static RuntimeOptions FromSessionConfig(
string datDir,
Func<string, string?> env,
string sessionConfigPath,
SessionConfiguration config,
SessionDescriptor session,
string? resolvedPassword)
{
if (config is null) throw new ArgumentNullException(nameof(config));
if (session is null) throw new ArgumentNullException(nameof(session));
ArgumentException.ThrowIfNullOrWhiteSpace(sessionConfigPath);
RuntimeOptions baseOptions = Parse(datDir, env);
SessionContentDescriptor? content = config.Process?.Content;
return baseOptions with
{
PreparedAssetPath = NullIfEmpty(content?.PreparedAssetPath)
?? baseOptions.PreparedAssetPath,
LiveMode = true,
LiveHost = session.Endpoint.Host,
LivePort = session.Endpoint.Port,
LiveUser = session.Account,
LivePass = resolvedPassword,
SessionConfigPath = sessionConfigPath,
SessionId = session.Id,
LiveCharacterSelector = MapCharacterSelector(session.Character),
StatusFilePath = NullIfEmpty(session.StatusFile),
Plugins = session.Plugins,
LoginCommands = (IReadOnlyList<string>?)session.LoginCommands ?? [],
LoginCommandDelayMs = session.LoginCommandDelayMs,
};
}
private static LiveSessionCharacterSelector? MapCharacterSelector(
SessionCharacterSelectorDescriptor? selector) =>
selector is null
? null
: new LiveSessionCharacterSelector(
selector.Index,
selector.Id,
selector.Name);
/// <summary>True iff live-mode credentials are present and valid for connecting.</summary>
public bool HasLiveCredentials =>
LiveMode && !string.IsNullOrEmpty(LiveUser) && !string.IsNullOrEmpty(LivePass);