21 KiB
Modern runtime Slice K — Linux headless and multi-session host
Status: K0–K3 COMPLETE; K4 IMPLEMENTED, CONNECTED TERMINAL GATE 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:
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:
NetClientuses the WindowsSIO_UDP_CONNRESETsocket control code only behindOperatingSystem.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
RuntimeOptionsis App-owned and cannot be reused; - no production headless session host or typed bot command adapter exists;
WorldSessionuses wall-clock deadlines and a fixedThread.Sleep(200)during connect; scheduler/timing ownership needs an explicit audit;- a few Runtime/Core diagnostics still read environment variables at call sites;
RuntimeRemotePhysicsUpdaterreadsDateTime.UtcNow;- no multi-session mutable-static/source guard exists;
- each
RuntimePhysicsStatecurrently creates a productionPhysicsDataCache; 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
LiveSessionHostborrowingGameRuntime.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.Sleepappears 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
PathAPIs 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.HeadlessandAcDream.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, oneLiveSessionHost, 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 implementation is complete through 776482da. One process-level
absolute-deadline sampler 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.
The automated 1/5/10/30-root gate opens one exact process-content owner, isolates equal GUIDs, portal/reconnect generations, mutable Runtime/physics state, and policy faults, and converges every root and content lease. Its 30-root workload advances 480,000 frames per root (two simulated hours), 14.4 million turns total, with recurring movement, use, combat, casting, chat, portal, death, and reconnect. Five additional 30-root runs cancel at randomized connect/entry/activity/portal/reconnect/teardown edges.
The committed k4-linux-30-session envelope is diagnostic-only. It scales
private/resident memory, managed live/heap memory, CPU, handles, descriptors,
and sockets by configured session count while bounding threads, scheduler
wakeups/catch-ups, and deadline lateness process-wide. It names every
violation and requires exact Runtime/content convergence in the final
disposed sample; it never changes simulation, networking, scheduling, or
teardown.
Windows and native Ubuntu Headless tests pass 67/67; the complete Release
suite passes 8,784 tests / 5 skips. The exact 776482da native Linux process
has both available accounts in-world at Rynthid, zero faults/reconnect debt,
bounded wakeups and CPU, and every sampled dimension inside the committed
ceiling. The user requested that it remain running for observation; its
graceful disposed sample is the sole remaining K4/Slice-K closeout gate.
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 <sha> 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:
git revert aada8a37c1e933f9f0a3f41dc4f05615b01023bc
K1 exact rollback:
git revert f8cb840fb15b27f61afa5c33cad6c4dba8584949
K2 exact rollback, newest first:
git revert 38e83640d907ee3e0819f3b38ca117ca60914d6b
git revert 7e8acb74dd20931d5ba88430fd100cd074a32e4d
K3 rollback, newest first:
git revert 3f3401257c83bdd9f557eb577ba08cf2e5ec9a06
git revert b6547ff38c375328417855200f41c8af6436f246
git revert 9569dadb578b299b24e30aec85eee86ec0c2b3fb
git revert 12b500d38362e8aae0a23a4f9712b86367006f76
K4 rollback, newest first:
git revert 776482da8225d88a7c6128022d137d26f9266a87
git revert bd236ce553718cfb1248703f4a2b17b35733b11c
git revert 97c174fbb2d72fb94e2fe07448bb257989ed8941
git revert cb512fd0916fbf49285c9b64df1630c5a69a0d20