merge: headless console — interactive chat/command CLI for the bot host (review-closed)

Owner direction 2026-09-07. Reader thread → tick-drained queue, the same
ChatCommandRouter.Submit the chat box uses, event-stream renderer,
SpewBox pump, --console / ACDREAM_HEADLESS_CONSOLE (=0 disables).
Opus review APPROVE-WITH-FIXES, 12-item fix round, narrow re-check
MERGE-READY.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 08:25:22 +02:00
commit 8cb284d6f7
15 changed files with 1728 additions and 17 deletions

View file

@ -40,12 +40,16 @@ Assume a flag has a side effect until its row says otherwise.
- **Everything diagnostic is OFF by default.** Every probe, dump, capture,
and measurement flag in this document is inert until its variable is
explicitly set — an unset environment runs zero diagnostics. Exactly
five flags default ON, and none is a diagnostic: `ACDREAM_RETAIL_CHASE`,
six flags default ON, and none is a diagnostic: `ACDREAM_RETAIL_CHASE`,
`ACDREAM_CAMERA_COLLIDE`, `ACDREAM_CAMERA_ALIGN_SLOPE`, and
`ACDREAM_RETAIL_CLOSE_DEGRADES` are retail *behaviors* wearing an A/B
off-switch (`=0` disables the behavior for a comparison run), while
`ACDREAM_RETAIL_UI` is the product's only gameplay presentation and uses
the same explicit diagnostic opt-out. That five-flag set is frozen by
the same explicit diagnostic opt-out. `ACDREAM_HEADLESS_CONSOLE` is the
sixth: its unset default is terminal-shaped (on when stdin is a real
console, off when redirected — not unconditionally on like the other
five), but once the variable is SET AT ALL it uses the identical `=0`
override (any other value enables). That six-flag set is frozen by
`LaunchOptionsDocumentationTests` — a new
default-on flag fails the build.
- `=1` means the code tests for exactly the string `1`. Setting `true`,
@ -93,6 +97,7 @@ dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Release
| `ACDREAM_DAT_DIR` | `=<path>` | Fallback dat-directory when no positional argument is given. App: single read at `Program.cs:58`. Cli: read independently per-subcommand (each subcommand does `args.ElementAtOrDefault(N) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")`) plus once more for the default (no-subcommand) asset-inventory mode at line 152. | Two of the four `Program.cs` line numbers in the raw grep (91, 135) are **not reads** — they're the literal string `ACDREAM_DAT_DIR` inside `Log.Error` usage-text messages, not `GetEnvironmentVariable` calls. Only line 58 is a real read in `AcDream.App`. | none — hard usage error (exit 2) if unset and no positional arg | `Program.cs:58` (App); `Cli/Program.cs:24,35,47,59,71,84,113,125,137,152` (every Cli subcommand) |
| `ACDREAM_DISPLAY_PROTOCOL` | `="auto"` / `"x11"` / `"wayland"` (case-insensitive, trimmed); any other value throws `InvalidOperationException` at startup | Linux-only: forces the GLFW 3.4 platform-init hint (X11 vs Wayland vs auto) before any window is created; ignored entirely on Windows (always `Windows` protocol) | An invalid value is fatal at startup (throws before any window exists), not a silent fallback | unset → auto-detected from `XDG_SESSION_TYPE`/`WAYLAND_DISPLAY`/`DISPLAY`, falling back to GLFW `Automatic` | `GraphicalWindowBackendSelection.Resolve` (`GraphicalWindowBackendSelection.cs:26-58`) |
| `ACDREAM_FAR_RADIUS` | `=<int>` | Overrides preset's `FarRadius` (outer streaming/reveal window, landblocks) | Enlarging changes streaming memory budget and what's resident/rendered — CLAUDE.md: leave unset for measurement/gate runs (same family as legacy `ACDREAM_STREAM_RADIUS`) | preset's `FarRadius` (Low=5, Medium=8, High=12, Ultra=15) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:47`) |
| `ACDREAM_HEADLESS_CONSOLE` | `=0` disables (once set at all); any other value enables; unset falls through to the terminal-shaped default | Turns on the headless host's interactive console (docs/plans/2026-09-07-headless-console.md): a background thread reads stdin lines, each drained on the session tick through the SAME plugin-verb/client-slash-command pipeline the graphical chat box uses, with chat/lifecycle/portal output rendered to stdout. Only takes effect for `run` with a single configured session — a multi-session process reports `console: single-session only` via the diagnostics stream and does not attach one. | Starts a background stdin-reader thread and writes plain-text lines to the same stdout stream `HeadlessDiagnosticWriter` already uses for its JSON lines — the two interleave. Only applies to `run`; `--console` (bare flag, no value) always wins over this variable. S1 fix (2026-09-07): the variable itself now wins outright once SET AT ALL — `=0` disables even when stdin is a real terminal, matching every other `=0`-disables flag in this table; only an UNSET variable falls through to the terminal-shaped default. | unset → on when stdin is a real console, off when redirected (`!Console.IsInputRedirected`, checked once in `Program.cs`); set → `!= "0"` | `HeadlessConsoleOptions.Resolve` (`Configuration/HeadlessConsoleOptions.cs`) → `HeadlessEntryPoint.Run``HeadlessProcessHost`'s `consoleEnabled` |
| `ACDREAM_LIVE` | `=1` (exactly the literal string `"1"`) | Core switch: connect to a live ACE server instead of running offline/no-connect. | The 4 non-`RuntimeOptions.cs` line numbers in the raw grep are **all comments or log-message text**, not reads — `SessionStartComposition.cs:39` is inside the string `"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"`; `Program.cs:126` is inside a `--session-config` override log line; `GameWindow.cs:614,627` are doc comments. The only actual parse is `RuntimeOptions.cs:141`. Requires `ACDREAM_TEST_USER`/`ACDREAM_TEST_PASS` too (`HasLiveCredentials`) or the session silently reports `MissingCredentials` and skips. Forced to effectively-on (LiveMode=true) unconditionally by `--session-config` launches regardless of this var. | `false` | `RuntimeOptions.LiveMode``SessionStartComposition.cs` (log text only), `Program.cs:126` (log text only), `GameWindow.cs:614,627` (comments only), consumed for real via `RuntimeOptions.HasLiveCredentials` and `WorldSession`/`GameRuntime` session-start gating |
| `ACDREAM_MAX_COMPLETIONS_PER_FRAME` | `=<int>` | Overrides preset's per-frame streaming-completion throughput cap | Directly changes the streaming admission budget measured by perf/completion gates — do not vary during a measurement run | preset's value (Low=2, Medium=3, High=4, Ultra=6) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:59`) |
| `ACDREAM_MSAA_SAMPLES` | `=<int>` (0/2/4/8) | Overrides preset's MSAA sample count | Changes GPU multisample anti-aliasing (visual + GPU-cost change) | preset's `MsaaSamples` (Low=0, Medium=2, High/Ultra=4) | `QualitySettings.WithEnvOverrides` (`QualityPreset.cs:48`) |
@ -143,6 +148,7 @@ config without connecting; `run` connects.
| `--config <path>` | The versioned headless session-configuration document. Required. | — |
| `--config-dir` / `--data-dir` / `--cache-dir` `<path>` | Override each portable path root. | Merged over the config document's own `process.paths`; the command line wins. |
| `-user` / `--user`, `-password` / `--password` | Direct single-session credentials, bypassing the config's credential source. | Plaintext in the process command line — prefer the config's credential reference. |
| `--console` | Forces the interactive console on for `run` (bare flag, no value) — see `ACDREAM_HEADLESS_CONSOLE`. | Same side effects as the environment variable; this flag always wins over it. |
| `--help` / `-h` (or no args) | Prints usage, exits 0. | — |
### `AcDream.Launcher`

View file

@ -66,3 +66,158 @@ it or the lead may, it is not a visual gate.
## Ledger
- 2026-09-07 planned; implementer dispatched.
- 2026-09-07 IMPLEMENTED. The dispatch seam already existed:
`AcDream.Runtime.Chat.ChatCommandRouter.Submit` is the SAME presentation-
free pipeline `LoginCommandSequence` (headless) and every graphical chat
window (`ChatWindowController`, `FloatingChatWindowController`,
`RetailUiRuntime`) already call — no lift was needed. Added
`HeadlessSessionHost.SubmitConsoleLine` (`Hosting/HeadlessSessionHost.cs`)
as the one new call site, reusing the host's own retained
`LiveChatCommandSurface`/plugin registry (now promoted from ctor locals to
fields) instead of a second parser.
New files: `Configuration/HeadlessConsoleOptions.cs` (typed `--console` /
`ACDREAM_HEADLESS_CONSOLE=1` / terminal-default resolution),
`Hosting/HeadlessConsoleInputReader.cs` (background stdin thread → FIFO
queue, never executes handler code), `Hosting/HeadlessConsoleController.cs`
(drains the queue on the session tick via a new `HeadlessSessionHost.
ConsolePump` hook; owns `/quit`/`/status`), `Hosting/
HeadlessConsoleChatFormatter.cs` + `Hosting/HeadlessConsoleRenderer.cs`
(renders the K2 bot event stream — `IRuntimeEventObserver`, the same
interface a bot policy subscribes — as bracket-labelled lines:
`[Tell] Bob: hi`, `[Fellowship] …`, `[Local] …`), `Hosting/
HeadlessConsoleChatFeedback.cs` (decorates `RuntimeChatCommandFeedback` so
retail's transient SpewBox/`ClientLocal` interface text — which never
touches `ChatLog`, so it never reaches the K2 event stream — also reaches
the console). `/quit` cancels a `CancellationTokenSource` linked into the
scheduler's run token in `HeadlessProcessHost` (the SAME graceful-exit
path an external Ctrl+C/SIGTERM already takes); `/status` reports
generation, position (or "unknown" without a live movement controller),
and loaded-plugin count (no plugin today reports a richer macro-state
string). Console only attaches for a single-session `run` (per the plan's
"out of scope for the first cut" multi-session note); constructed AFTER
every session's own credential resolution so the reader thread never
races a `StandardInput`-provider password prompt on the same stream.
Chosen console default: on when `!Console.IsInputRedirected` (a real
operator at a terminal), off when redirected (scripts/CI/piped fixtures,
where a blocked `ReadLine` on a background thread would just sit idle) —
resolved once in `Program.cs`, the only place that can see the real
`Console`.
Deviation from the plan's illustrative example: retail's own transcript
never prefixes Tell/Local lines with a bracket (`ChatVM.FormatEntry`
renders "Bob tells you, ..."/"Bob says, ..." with no label) — Headless
cannot reference `AcDream.UI.Abstractions` (the dependency-boundary
test), so `HeadlessConsoleChatFormatter` is a deliberately DIFFERENT,
terminal-shaped "[Label] Sender: text" rendering using the SAME channel-
name strings (matching the plan's literal `[Tell] Bob: hi` example), not
a byte-for-byte port of the graphical prose.
Tests: `tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs` (20 new
tests — options resolution, command-line flag parsing, reader-thread
ordering/never-on-reader-thread, controller drain/quit/status, chat
formatting, and full `/say`/plain-text/plugin-verb/unknown-verb dispatch
against a real `HeadlessSessionHost` + `FixtureSessionOperations`, no live
server) plus the existing `LaunchOptionsDocumentationTests` (4/4 green)
and `HeadlessDependencyBoundaryTests` (3/3 green, unchanged — Headless
still references only `AcDream.Runtime`). Every test in this batch was
mutation-checked to fail before the corresponding production line existed
(see the implementer's final report for the specific mutations run:
skipping the interface-text callback, skipping `_quitRequested.Cancel()`,
forcing `TryHandlePluginCommand` to always return false, dropping the
reader thread's `Enqueue`, and swapping `ChatChannelKind.Say` for `.Tell`
in `SubmitConsoleLine`).
Suites: `dotnet test tests/AcDream.Headless.Tests -c Release` → 193
passed / 1 pre-existing failure (`LinuxRejectsGroupOrOtherCredentialPermissions`,
a Linux-only lane test that cannot run on this Windows host — unrelated
to this change) / 194 total. `dotnet test tests/AcDream.Runtime.Tests -c
Release` → 1891/1891 passed. `dotnet test tests/AcDream.App.Tests -c
Release --filter "FullyQualifiedName~Chat|FullyQualifiedName~Command|
FullyQualifiedName~LaunchOptions"` → 414 passed / 2 pre-existing failures
(`ChatIndicatorButtonLiveMountProbeTests`/`OptionsPanelLiveMountProbeTests`
— both gated on `ACDREAM_PROBE_LIVE_MOUNT=1`, a manual live-DAT probe lane,
unrelated to this change) / 3 skipped / 419 total. `dotnet build
AcDream.slnx -c Release` green throughout.
- 2026-09-07 FIX ROUND (Opus review, APPROVE-WITH-FIXES). S1: `ACDREAM_
HEADLESS_CONSOLE=0` now disables the console even when stdin is a real
terminal — the prior `== "1"` test let `"0"` silently fall through to the
terminal-shaped default; the flag is now the sixth entry in
`LaunchOptionsDocumentationTests.DefaultOnBehaviorFlags` (a default-on
behavior with an A/B off-switch, once set at all, like
`ACDREAM_RETAIL_CHASE`). S2: the reader-thread pin is now falsifiable — a
fixture `TextReader` records the actual thread id `ReadLine` ran on, and a
new test asserts the controller's submit callback runs on neither that
thread nor any other unexpected one, only the `DrainDue` caller's. S3: one
`HeadlessProcessHost` end-to-end test proves a console line reaches the
session's real `SubmitConsoleLine` pipeline and `/quit` returns
`HeadlessExitCode.Success`. S4: `HeadlessConsoleController.Handle` now
wraps `_submit` in try/catch (mirroring `LoginCommandSequence.DrainDue`)
and prints a line for `UnknownCommand`/`Dropped`, so a console typo can
never escape into the scheduler's per-session quarantine catch. S5:
deleted the per-call `HeadlessConsoleChatFeedback` decorator — it only
ever saw text produced by the console's OWN `SubmitConsoleLine` calls.
The new `HeadlessConsoleSpewBoxPump` polls the shared `SpewBoxState` on
the console's own per-tick pump instead, the SAME seam the graphical
overlay's `SpewBoxController.Tick` reads, so server- and plugin-driven
`ClientLocal` interface text prints too. S6: `Program.cs` now resolves
`standardOutputIsTerminal` next to the stdin probe and threads it through
`HeadlessEntryPoint.Run``HeadlessProcessHost`, which no longer reads
`System.Console.IsOutputRedirected` itself. S7: a multi-session process
launched with `--console` now reports `_diagnostics.Message("console",
"single-session only")` instead of silently skipping console attachment.
N1: corrected two stale dispatch-order doc comments
(`HeadlessSessionHost.SubmitConsoleLine`, `HeadlessConsoleController`'s
class remarks) to the real `ChatCommandRouter.Submit` order: retail's
client-command catalog, local `/help`, plugin verbs, the unregistered-
channel-tag fallback, an explicit server command, then plain chat. N2:
`HeadlessCommandLine.Console` renamed to `ConsoleEnabled`. N3: `validate`
mode now rejects `--console` outright rather than silently ignoring it.
N4: **`/status` and `/quit` are console-intercepted verbs — they never
reach `ChatCommandRouter`, unlike `@status`, which is a real server
command and still passes through untouched.** N5: `HeadlessConsoleRenderer`
now dims only lifecycle/command/portal lines; chat and interface text
print at the terminal's default weight.
Every new/changed test was shown to fail first against a targeted
mutation of the corresponding production code (see each commit's own
body for the specific mutation) before the fix landed; one commit per
item, all with `Co-Authored-By: Claude Fable 5.1`.
Suites (Release): `dotnet test tests/AcDream.Headless.Tests` → 207
passed / 1 pre-existing Linux-lane failure
(`LinuxRejectsGroupOrOtherCredentialPermissions`) / 208 total (up from
193/1/194 before this round — 14 new/changed tests). `dotnet test
tests/AcDream.App.Tests --filter "FullyQualifiedName~LaunchOptions"` →
4/4 passed, including the corrected `OnlyTheSixProductBehaviorFlagsDefaultOn`
(renamed from Five). `dotnet build AcDream.slnx -c Release` green
throughout.
### Connected proof recipe (owner runs; NOT run by the implementer)
Against a running local ACE at `127.0.0.1:9000` with MossTank loaded for
the second half:
```powershell
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
dotnet run --project src\AcDream.Headless\AcDream.Headless.csproj --no-build -c Release -- `
run --config <path-to-a-one-session-config.json> `
-user testaccount -password testpassword --console
```
The referenced config's one session should target character `+Acdream`
(server guid `0x5000000A`) against `127.0.0.1:9000`, an `idle` bot policy,
and (for the second half) the MossTank plugin id under `plugins`. Once the
console prints `entered world`:
1. Type `/say hello` and press Enter — expect the SAME line ACE echoes back
to any other observer (a retail client or a second acdream session
watching `+Acdream`) to also print `[Local] You: hello` in this console
(the server's own HearSpeech echo, rendered through the normal chat
event stream).
2. Type `/status` — expect a line with `generation=`, `position=` (a real
cell/local-frame triple once in world), and `plugins=N loaded`.
3. With MossTank loaded, type `/vt start` (or whatever verb MossTank
registers) — expect MossTank's own handler to run (check its own
status/log output) and confirm NOTHING was sent to the wire for that
line (no `@vt` server command).
4. Type `/quit` — expect a graceful ACE logout (same as the existing
Ctrl+C behavior) and the process to exit 0.
This is not a visual gate; the owner (or the lead) runs it opportunistically
before considering the plan CLOSED.