wip: Campaign LA LA2 probe mode + idle policy — INCOMPLETE, stopped mid-task
Agent was stopped for token budget. Landed here: probe flag through LiveSessionConnectOptions + the StartCore short-circuit, the mode field with JsonRequired-to-semantic-validation move, host exit-code mapping, and 34 passing tests including 3 new probe tests (agent last reported green before the stop). NOT DONE: the idle-policy unit tests (next step), full-suite verification, and the WSL run. Build/test state UNVERIFIED at this commit. Next session: finish idle policy tests, run Runtime+Headless Release suites Windows and WSL, then dispatch the Opus dual-lens review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
498f1c1182
commit
c601942467
14 changed files with 830 additions and 35 deletions
|
|
@ -46,11 +46,33 @@ internal sealed record HeadlessSessionDescriptor
|
|||
[JsonRequired]
|
||||
public string Account { get; init; } = string.Empty;
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessCharacterSelector Character { get; init; } = new();
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA2: ABSENT (<see langword="null"/>) for normal play
|
||||
/// sessions; <see cref="HeadlessSessionMode.Probe"/> for the LA2 probe
|
||||
/// (connect → characterList → graceful disconnect, never EnterWorld) —
|
||||
/// the pinned launch-contract schema's <c>mode</c> field
|
||||
/// (<c>docs/plans/2026-08-14-launcher-campaign.md</c> LA1/LA2).
|
||||
/// <see cref="Character"/>/<see cref="Policy"/> requiredness depends on
|
||||
/// this value, which is why their requiredness lives in
|
||||
/// <see cref="HeadlessConfigurationLoader"/>'s semantic validation rather
|
||||
/// than a <c>[JsonRequired]</c> attribute — that attribute fires during
|
||||
/// deserialization, before <see cref="Mode"/> can be inspected at all.
|
||||
/// </summary>
|
||||
public HeadlessSessionMode? Mode { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessBotPolicyDescriptor Policy { get; init; } = new();
|
||||
/// <summary>
|
||||
/// Required for play sessions (<see cref="Mode"/> absent); MUST be
|
||||
/// omitted for probe sessions (<see cref="HeadlessSessionMode.Probe"/>) —
|
||||
/// the pinned contract keeps the shape unambiguous by forbidding a probe
|
||||
/// session from also declaring a selector. Enforced by
|
||||
/// <see cref="HeadlessConfigurationLoader.ValidateSession"/>, not
|
||||
/// <c>[JsonRequired]</c> (see this record's own doc on <see cref="Mode"/>).
|
||||
/// </summary>
|
||||
public HeadlessCharacterSelector? Character { get; init; }
|
||||
|
||||
/// <summary>Same mode-dependent requiredness as <see cref="Character"/>:
|
||||
/// required for play sessions, forbidden for probe sessions.</summary>
|
||||
public HeadlessBotPolicyDescriptor? Policy { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public HeadlessCredentialReference Credential { get; init; } = new();
|
||||
|
|
@ -140,6 +162,19 @@ internal sealed class HeadlessBotPolicyDescriptor
|
|||
public HeadlessBotPolicyRole? Role { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA2: see <see cref="HeadlessSessionDescriptor.Mode"/>.
|
||||
/// The pinned launch-contract schema defines exactly two states for a
|
||||
/// session — ABSENT (mapped to <see langword="null"/>, meaning "play") or
|
||||
/// the literal string <c>"probe"</c> — so <see cref="Probe"/> is the only
|
||||
/// member; there is no explicit "play" spelling.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<HeadlessSessionMode>))]
|
||||
internal enum HeadlessSessionMode
|
||||
{
|
||||
Probe,
|
||||
}
|
||||
|
||||
/// <summary>See <see cref="HeadlessBotPolicyDescriptor.Role"/>.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<HeadlessBotPolicyRole>))]
|
||||
internal enum HeadlessBotPolicyRole
|
||||
|
|
|
|||
|
|
@ -182,6 +182,57 @@ internal static class HeadlessConfigurationLoader
|
|||
$"Session '{session.Id}' requires a non-empty account.");
|
||||
}
|
||||
|
||||
ValidateModeShape(session);
|
||||
|
||||
if (session.Credential is null
|
||||
|| string.IsNullOrWhiteSpace(session.Credential.Reference))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a credential reference.");
|
||||
}
|
||||
|
||||
ValidateCharacterOptions(session);
|
||||
ValidateLaunchContractFields(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA2: mode-dependent requiredness for
|
||||
/// <see cref="HeadlessSessionDescriptor.Character"/>/
|
||||
/// <see cref="HeadlessSessionDescriptor.Policy"/> — this REPLACES the
|
||||
/// former `[JsonRequired]` attributes on both properties (which fired
|
||||
/// unconditionally at deserialize time, before a probe session's
|
||||
/// omission could ever be distinguished from a play session's mistake).
|
||||
/// A play session (mode absent) keeps EXACTLY today's strictness: a
|
||||
/// missing/malformed character selector or a missing policy id still
|
||||
/// fails load, just via <see cref="HeadlessConfigurationException"/>
|
||||
/// naming the field instead of a raw <see cref="JsonException"/> citing
|
||||
/// "missing required properties" — same exit code (3,
|
||||
/// <c>HeadlessExitCode.ConfigurationError</c>) either way, more specific
|
||||
/// text now (an accepted improvement, not a contract change). A probe
|
||||
/// session (mode "probe") must OMIT both fields entirely — the pinned
|
||||
/// contract keeps the shape unambiguous by rejecting a probe session
|
||||
/// that also declares a selector or a policy, rather than silently
|
||||
/// ignoring them.
|
||||
/// </summary>
|
||||
private static void ValidateModeShape(HeadlessSessionDescriptor session)
|
||||
{
|
||||
if (session.Mode == HeadlessSessionMode.Probe)
|
||||
{
|
||||
if (session.Character is not null)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' has mode \"probe\" and must omit "
|
||||
+ "'character' — a probe never selects a character.");
|
||||
}
|
||||
if (session.Policy is not null)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' has mode \"probe\" and must omit "
|
||||
+ "'policy' — a probe never drives a bot policy.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.Character is null)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
|
|
@ -206,16 +257,6 @@ internal static class HeadlessConfigurationLoader
|
|||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a non-empty policy id.");
|
||||
}
|
||||
|
||||
if (session.Credential is null
|
||||
|| string.IsNullOrWhiteSpace(session.Credential.Reference))
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
$"Session '{session.Id}' requires a credential reference.");
|
||||
}
|
||||
|
||||
ValidateCharacterOptions(session);
|
||||
ValidateLaunchContractFields(session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue