# Modern runtime Slice K — Linux headless and multi-session host **Status:** K0–K3 COMPLETE; K4 ACTIVE 2026-07-27 **Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice K **Authorization:** the user approved Slices F–L on 2026-07-24 **Purpose:** run efficient automated clients on Linux without a display, GPU, audio device, hidden graphical process, or duplicated gameplay model, then prove 30 isolated sessions can share immutable content safely in one process. ## 1. Entry contract Slice K starts after J8 proves one direct `GameRuntime` can connect, enter world, receive authoritative state, move, use, fight/cast, portal, stop, reconnect, and converge without presentation assemblies. That entry gate passed at `a9a822f2`: 395 Runtime tests, 3,731 App tests / 3 skips, 8,696 complete-solution tests / 5 skips, exact-binary connected lifecycle/reconnect, and canonical nine-stop routes all pass. K adds a production host over that same root. It does not move gameplay out of App again and does not create a bot-specific world model. Dependency direction: ```text AcDream.Headless ──► AcDream.Runtime ├──► AcDream.Core.Net ├──► AcDream.Core ├──► AcDream.Content └──► AcDream.Plugin.Abstractions AcDream.Headless -X► AcDream.App / AcDream.UI.* / Silk.NET / OpenAL / ImGui ``` The existing `AcDream.Cli` is asset/performance tooling with ImageSharp and DAT inspection dependencies. It is not renamed or expanded into the bot process. Create a dedicated `AcDream.Headless` executable and matching test project. ## 2. Initial platform inventory The Runtime/Core.Net/Content closure is already mostly portable: - `NetClient` uses the Windows `SIO_UDP_CONNRESET` socket control code only behind `OperatingSystem.IsWindows()`. - retail CP1252 encoding is provided by .NET code pages and is not a Windows OS dependency. - Runtime has no App, Silk, OpenAL, Arch, ImGui, or native-window reference. - `PakReader`, prepared collision, and immutable content contracts are presentation-independent. Known work: - no ordinary Linux build/test workflow exists; - no portable application-path/config owner exists; - graphical `RuntimeOptions` is App-owned and cannot be reused; - no production headless session host or typed bot command adapter exists; - `WorldSession` uses wall-clock deadlines and a fixed `Thread.Sleep(200)` during connect; scheduler/timing ownership needs an explicit audit; - a few Runtime/Core diagnostics still read environment variables at call sites; - `RuntimeRemotePhysicsUpdater` reads `DateTime.UtcNow`; - no multi-session mutable-static/source guard exists; - each `RuntimePhysicsState` currently creates a production `PhysicsDataCache`; shared immutable collision content versus per-session mutable cache/scratch must be made explicit; - no long-duration, 30-session process resource ledger exists. ## 3. Fixed host model ### Process owner One `HeadlessProcessHost` owns: - parsed immutable process options; - portable paths; - credential provider; - shared read-only DAT/prepared-content/collision sources; - one deterministic scheduler; - a registry of session hosts keyed by configured bot identity; - process cancellation and terminal diagnostics. ### Session owner Each `HeadlessSessionHost` owns exactly: - one `GameRuntime`; - one headless host lease; - one `LiveSessionHost` borrowing `GameRuntime.Session`; - one session command adapter implementing `IGameRuntimeCommands`; - one event subscription; - one bot policy/plugin instance; - per-session clock deadlines, sequence state, query scratch, and cancellation; - an exact retryable teardown cursor. Sessions never share: - `GameRuntime`, session/generation, local-player identity; - entity directory, object table, inventory, character, communication, action, movement, environment, transit, or event sequencer; - physics engine, moving bodies, transition scratch, projectile workset, or spatial queries; - mutable bot/plugin state; - credentials or logs containing credentials. Sessions may share only immutable, thread-safe content: - mapped `acdream.pak`; - read-only DAT databases behind a proven synchronization/reader contract; - immutable spell/skill/region tables; - immutable flat collision payloads. Sharing a cache is permitted only when both its values and all bookkeeping mutations are thread-safe and session-neutral. Otherwise each session gets its own bounded cache over the shared immutable source. ## 4. Scheduler and timing contract Use one monotonic process scheduler, not one busy loop or dedicated OS thread per bot. - Each session publishes its next absolute deadline. - The scheduler waits until the earliest deadline or cancellation/wakeup. - Due sessions tick in stable configured order. - A bounded catch-up loop preserves elapsed-time behavior without a tick-count clock. - One slow session cannot make another session skip generation validation or accepted packet order. - Network receive may remain asynchronous, but every accepted inbound callback for a session is serialized through that session's Runtime turn. - Bot policy runs only after accepted inbound and simulation work for that turn, then emits typed commands synchronously. - No `Thread.Sleep` appears in the steady scheduler. Wire cadence uses the instance clock and absolute elapsed deadlines. It never derives AutonomousPosition or MoveToState emission from “every N scheduler ticks.” The gate compares: - AutonomousPosition inter-packet intervals; - MoveToState-on-change emission points; - use/combat/cast request order; - portal LoginComplete placement; against the graphical host under the same deterministic scenario and a committed tolerance band. ## 5. Configuration, paths, and credentials ### Portable paths Introduce a BCL-only `HeadlessPathSet`: - Linux config: `$XDG_CONFIG_HOME/acdream`, else `~/.config/acdream`; - Linux data: `$XDG_DATA_HOME/acdream`, else `~/.local/share/acdream`; - Linux cache: `$XDG_CACHE_HOME/acdream`, else `~/.cache/acdream`; - Windows equivalents use the appropriate `Environment.SpecialFolder`; - explicit command-line paths override defaults; - all paths use `Path` APIs and are normalized before use. ### Configuration Use a versioned JSON document with process settings plus session descriptors. Unknown properties are rejected by default. Each session has stable ID, endpoint, character selector, bot policy/plugin ID, and credential reference. Passwords never appear in the ordinary process config. ### Credentials Support non-interactive providers suitable for local automation: - environment-variable reference; - standard-input secret stream; - credential file with an explicit user-only-permission check on Linux. The user additionally requires an explicit `-user`/`-password` (and long-form alias) launch override for local single-session automation. It is accepted only for `run` with exactly one configured session and intentionally carries the ordinary process-list exposure of command-line arguments. Configuration-based multi-session production continues to use the providers above. `ToString`, validation errors, logs, checkpoints, crash reports, and metrics never echo the password, and the session wraps the value in the same disposable credential owner used by provider-based launch. ## 6. Bot surface The public bot API is the existing stable seam: - `IGameRuntimeView`; - `IGameRuntimeCommands`; - `IRuntimeEventSource`; - `RuntimeStateCheckpoint`; - generation and teardown acknowledgements. Add a headless command adapter that maps those typed commands to the same Runtime owners and `WorldSession` sends used by the graphical adapter. It may provide host operations for target queries or decoded content lookups, but it must not expose mutable owner internals or reproduce App's selection/render world. Minimum bot commands/events: - movement and stance; - selection and target cycling; - use, appraisal, pickup, and looting; - combat mode, attack, and spell cast; - portal recalls; - inventory/shortcut/spellbook/character/social mutations; - chat and slash/server commands; - lifecycle, entity, inventory, chat, movement, portal, combat, and command outcomes. Commands are generation-gated, synchronous at the Runtime seam, and inert as soon as session teardown begins. ## 7. Execution ### K0 — portability boundary and Linux CI **Complete at `aada8a37`.** The dedicated `AcDream.Headless` executable and test project reference only Runtime, strict no-connect configuration validation runs on Windows and Linux, dependency/loaded-assembly guards forbid graphical and audio backends, and the Windows/Ubuntu workflow builds/tests only the portable closure. Local Ubuntu 24.04 gates pass 600 Core.Net, 119 Content, 395 Runtime, and 12 Headless tests. The exact Windows Release solution passes 8,708 tests / 5 intentional skips. - Add `AcDream.Headless` and `AcDream.Headless.Tests`. - Add dependency-closure and loaded-assembly guards. - Add a normal GitHub Actions matrix for Windows and Ubuntu. - Build/test Core, Core.Net, Content, Runtime, Plugin.Abstractions, Headless, and their tests on Ubuntu with no App restore/build requirement. - Audit and correct portable socket, encoding-provider, file-map, path, executable, and cancellation behavior. - Promote surviving per-call Runtime diagnostics into instance/process diagnostic options where necessary. Gate: Ubuntu restores, builds, tests, and runs `--help` plus a no-connect configuration validation without a graphical/audio/native-window dependency. ### K1 — portable config, credentials, and single-session CLI **Complete at `f8cb840f`.** The strict versioned configuration, XDG/Windows path owner, redacted environment/stdin/owner-only-file credentials, typed direct Runtime command route, canonical presentation-free inbound entity and portal route, production single-session host, deterministic exit codes, Ctrl+C/SIGTERM cancellation, and retryable teardown transaction are live. The explicit `lifecycle-smoke` policy proves connect, enter, inbound state, chat, lifestone recall, portal completion/LoginComplete, graceful stop, reconnect, second-generation materialization, and terminal convergence. Windows and Linux/WSL both passed that sequence against the same local ACE server. ACE retains its account/session index for roughly two seconds after confirming CharacterLogOff, so K1 gives the retiring production connection a one-time 2.5-second quiescence suffix before replacement. This is not a steady-state scheduler sleep; K2 replaces it with an absolute-deadline scheduler transition. - Implement portable paths and versioned strict config. - Implement redacted credential providers. - Compose one `GameRuntime`, one `LiveSessionHost`, one command adapter, one bot policy, and one teardown transaction. - Support start, graceful stop, reconnect, Ctrl+C/SIGTERM, and deterministic exit codes. - Emit structured credential-safe lifecycle/resource diagnostics. Gate: one Windows and one Linux no-window client connect to local ACE, enter world, receive state, issue a harmless command, portal, reconnect, and exit cleanly without loading presentation assemblies. ### K2 — deterministic scheduler and bot API parity **Complete at `7e8acb74` plus `38e83640`.** One monotonic absolute-deadline scheduler advances every configured session in stable order, uses bounded elapsed-time catch-up, and represents reconnect quiescence as a deadline. Graphical and headless hosts now use the same Runtime local-player frame controller, action owners, typed movement/selection/interaction/combat/ magic/state commands, hostile-target query, and ordered lifecycle/entity/ inventory/chat/movement/portal/combat/command event stream. The deterministic gate proves MoveToState/AutonomousPosition order and elapsed cadence, stale generation rejection, and 100,000 idle dispatch probes with zero turns and zero managed allocation. Runtime passes 411 tests, Headless 31, App 3,732/3 skips, and the complete Release solution 8,744/5 skips. Evidence: `docs/research/2026-07-27-slice-k2-deterministic-bot-parity.md`. - Add the absolute-deadline multi-session scheduler. - Serialize accepted per-session work while allowing sessions to progress independently. - Implement the complete typed bot command/event adapter. - Add deterministic graphical-versus-headless command, packet-order, and packet-timing traces. - Inject `TimeProvider`/monotonic clocks into remaining wall-clock Runtime reads that affect session behavior. Gate: command/event traces match; AutonomousPosition cadence and MoveToState-on-change fall within the committed tolerance; idle sessions do not busy-loop. ### K3 — shared immutable content and isolation **Complete at `12b500d3`, `9569dadb`, `b6547ff3`, and `3f340125`.** The process opens the exact DAT/pak/magic/terrain sources once, shares a bounded immutable flat-collision cache, and gives each session its own Runtime/physics/cache/movement/collision-neighborhood graph. The direct accepted-state route now hydrates local-player movement and terrain, EnvCell, building, stab, and procedural-scenery collision without loading presentation. The 1/5/10-session same-GUID/portal/reconnect gate and policy-fault quarantine pass. Runtime passes 412 tests, Headless 42, App 3,722/3 skips, and the complete Release solution 8,758/5 skips. The connected closeout adds explicit portal-route and long-lived observer policies as consumers of the same `IGameRuntimeView` / `IGameRuntimeCommands` seam used by graphical automation. It also aligns the direct host with retail position routing: ordinary accepted position echoes no longer re-run teleport placement or stop local motion, `ForcePosition` uses `BlipPlayer` semantics plus an immediate position acknowledgement, and a real teleport performs exactly one destination placement. High-volume collision snap diagnostics are now explicitly opt-in. The visible Windows production terminal passed connect, collision hydration, lifestone recall, graceful reconnect, second-generation materialization, and terminal teardown. The native Linux/WSL executable then passed direct CLI launch, exact Aerlinthe/Caul/Rynthid/lifestone routing, external `teleallto` portal generations, and a retail-client observer gate. The user saw the headless character turn and run smoothly without rubberbanding; three accepted external portal generations completed before ACE confirmed graceful logout. The final Release gate passes 412 Runtime tests, 47 Headless tests, 3,722 App tests / 3 skips, and 8,764 complete-solution tests / 5 skips. Evidence: `docs/research/2026-07-27-slice-k3-shared-content-isolation-checkpoint.md`. - Introduce a process content owner with leases and exact last-user disposal. - Share mapped pak/DAT/immutable tables and flat collision payloads. - Retain per-session mutable physics/cache/scratch/state. - Audit all mutable statics, thread-local caches, random sources, diagnostics, and process-wide identifiers in the Headless dependency closure. - Add 1/5/10-session connect, activity, portal, disconnect, reconnect, and GUID-collision tests in one process. - Add plugin/bot-policy fault isolation. Gate: equal server GUIDs in different sessions remain isolated; one failing session/plugin cannot corrupt or stop another; shared content opens once and disposes only after the final host acknowledgement. ### K4 — 30-session stress and closeout K4 telemetry checkpoint `cb512fd0` is complete. One process-level absolute-deadline sampler now reports CPU, private/resident/managed memory, GC, threads, handles, Linux descriptors/sockets, session/ownership debt, shared-content leases, scheduler wakeups, and deadline lateness without adding per-session workers or timers. The first native Linux two-account run exposed and corrected both pre-connect deadline arming and a sub-millisecond `Task.Delay` busy loop. Windows and native Ubuntu Headless tests pass 56/56; the complete Release suite passes 8,773 tests / 5 skips. The user requested that the active two-client observer process remain running, so the exact fixed- binary connected baseline and numeric ceilings remain next. Evidence: `docs/research/2026-07-27-slice-k4-resource-telemetry-checkpoint.md`. - Measure a 1-session warm baseline, then 5, 10, and 30 sessions. - Before the final run, commit numeric ceilings derived from K3 evidence for: - incremental private bytes per established session; - managed heap per session; - idle CPU per session and whole process; - handles, threads, timers, and sockets; - scheduler wakeups and maximum lateness; - reconnect/teardown debt. - Run at least two hours with 30 local-server sessions, including recurring movement, use, combat/cast, chat, portal, death/reconnect, and randomized cancellation. - Require stable plateaus over the final hour, no per-session creep, no busy-loop CPU, no credential exposure, and terminal convergence for all 30. - Update architecture, milestones, roadmap, issues/divergences, AGENTS/CLAUDE, and durable memory. Gate: 30 sessions in one Linux process for at least two hours, within committed numeric resource ceilings, with wire timing parity and clean teardown. Slice K then closes. ## 8. Adversarial matrix - missing XDG variables, unwritable home, relative paths, symlinked config, and paths containing spaces/non-ASCII; - invalid JSON version, duplicate session IDs, duplicate credential refs, and unknown fields; - password present in exception, log, metric label, process arguments, or crash output; - SIGINT/SIGTERM during connect, world entry, portal, combat, and teardown; - DNS failure, IPv4-only/IPv6 answers, port exhaustion, socket reset, packet loss, and delayed receive; - one session floods inbound while another is idle; - one bot policy blocks, throws, recursively commands, or retains a stale generation; - same server GUID and local IDs across different roots; - shared content disposed while another session reads; - mutable cache/scratch shared accidentally between sessions; - scheduler clock jump, long pause, catch-up, cancellation wakeup, and deadline tie; - 30 simultaneous reconnects and staggered teardown; - stale outbound route after reconnect; - plugin load/unload failure; - no renderer/audio/window allocation or assembly load. ## 9. Non-goals - No Linux graphical window, input, audio, or packaging; those are Slice L. - No GPU work. - No hidden/minimized App process. - No server-authoritative gameplay decisions on the client. - No bot-only shortcuts around the Runtime command/event seam. - No per-bot duplicate DAT/pak mapping unless evidence proves the shared reader cannot be made safe. - No service locator, global current-session singleton, or mutable static bot registry. ## 10. Commit and rollback discipline Land K0–K4 as separate buildable commits. Record the full production SHA and exact `git revert ` after each complete focused/Release/Linux/connected gate. No later sub-slice begins while the prior exact rollback is absent from this plan. K0 exact rollback: ```text git revert aada8a37c1e933f9f0a3f41dc4f05615b01023bc ``` K1 exact rollback: ```text git revert f8cb840fb15b27f61afa5c33cad6c4dba8584949 ``` K2 exact rollback, newest first: ```text git revert 38e83640d907ee3e0819f3b38ca117ca60914d6b git revert 7e8acb74dd20931d5ba88430fd100cd074a32e4d ``` K3 rollback, newest first: ```text git revert 3f3401257c83bdd9f557eb577ba08cf2e5ec9a06 git revert b6547ff38c375328417855200f41c8af6436f246 git revert 9569dadb578b299b24e30aec85eee86ec0c2b3fb git revert 12b500d38362e8aae0a23a4f9712b86367006f76 ```