docs(runtime): detail Linux multi-session host

Fix the portable host, scheduler, credential, shared-content, parity, and two-hour 30-session stress contracts before Slice K implementation begins.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 19:35:33 +02:00
parent 0a5bc68e70
commit 7d1f88da61
2 changed files with 335 additions and 0 deletions

View file

@ -1061,6 +1061,10 @@ host-parity tests after each move.
### Slice K — Linux headless and multi-session host
**Prepared execution plan:**
[`2026-07-26-modern-runtime-slice-k.md`](2026-07-26-modern-runtime-slice-k.md).
Implementation begins after Slice J's no-window closeout.
**Purpose:** Deliver efficient automated bots.
- Add Linux CI for Core, Core.Net, Content, Runtime, and Headless.

View file

@ -0,0 +1,331 @@
# Modern runtime Slice K — Linux headless and multi-session host
**Status:** PREPARED — BEGINS AFTER SLICE J CLOSEOUT
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice K
**Authorization:** the user approved Slices FL 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 only 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.
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.
Do not accept a plaintext password directly on the command line because process
lists expose it. `ToString`, validation errors, logs, checkpoints, crash
reports, and metrics contain only redacted credential IDs. Secrets are cleared
from retained buffers when the session is disposed where the BCL representation
allows it.
## 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
- 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
- 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
- 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
- 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
- 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 K0K4 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.