Paul Bergeron.
Menu

After Hours / Engineering handbook

The engineering behind the arcade.

Requirements you can trace. Decisions you can question. Behavior you can verify.

Case study · Architecture & operations · Live arcade ↗

Retrospective engineering record · September 9, 2026 · Independent public experiment

Follow the problem from requirements through implementation and verification. These documents describe the system that exists, the rationale behind its boundaries, and the evidence still needed. They do not claim a formal review board or production certification.

Product requirements

AH-PRD-001 · Implemented baseline · September 9, 2026

After Hours makes persistent agent behavior inspectable through a public arcade. A visitor should be able to see what an agent is doing, ask about it, and examine the engineering underneath. The underlying problem is coordinating model decisions, a changing environment, durable state and human messages when each operates at a different speed.

This is a retrospective specification of an independent project. It records the current product contract; it does not imply an earlier product committee, customer contract or formal design approval.

Users and the useful outcome

A visitor wants a working game and a natural conversation. An engineering reader wants to follow a decision from requirement to implementation to evidence. An operator needs to distinguish a healthy website from a working agent, stop control safely, and recover without discarding progress.

The product succeeds when the interaction is understandable and the system's behavior is explainable. Time spent on the page, contact conversions, game completion rate and response latency have not been measured as product outcomes.

Scope and acceptance criteria

ID Requirement Acceptance criterion Evidence / status
R1 Show real emulation Cabinet and focused view consume actual PNG frames associated with a resident/game; saved and provisional frames remain distinguishable. Streaming tests and public WebSocket checks passed.
R2 Preserve useful state Restart restores the last committed screen; switching away and back restores the resident's own game checkpoint. Recovery tests and all-15 catalog integration passed.
R3 Keep control attributable An old lease, wrong resident credential or public request cannot issue a valid game action. Authority, epoch and exclusion tests passed.
R4 Make chat delivery inspectable A retry with the same request ID does not create a second request; only explicit reply/dismiss resolves its pending status. Inbox tests; real visitor request received an answer.
R5 Offer a universal environment Each of three residents can choose any of fifteen installed titles when available; conflicting cabinet acquisition is rejected. Catalog and resident tests; live switches to uCity and Flooder observed.
R6 Operate independently of a visitor or laptop VM services own the CLI workers and game cores; closing the page does not own their lifecycle. VM sessions resumed and made actions with Mac launch agents disabled.
R7 Support accessible exploration Keyboard and touch move the visitor; all cabinet details are reachable through standard buttons; reduced motion retains a usable game view. Desktop dialogs, Escape and 390px mobile checks passed. Full assistive-technology audit remains open.
R8 Bound autonomous work Resident and manager launch allowances survive restarts; exhaustion prevents more launches until capacity returns or an authorized reset occurs. Manager budget tests and actual exhausted-ledger incident.

Constraints that shape the design

The project shares a VM with other applications. Public access uses existing HTTPS and a fixed edge route. Three active emulator cores cover the fifteen-game catalog; spectators do not create their own inference sessions. Model actions are based on screenshots and manuals, without game-memory reads. Only homebrew titles with reviewed license evidence are admitted, with pinned ROM hashes and attribution.

The selected runtime is GPT-5.6 Sol with low reasoning. Provider latency and quotas remain external dependencies. The work allowance limits scheduled turns, not dollars. A resident can be paused even while the website is healthy.

Deliberate boundaries

Visitors can explore, watch and talk; they do not acquire emulator control credentials. Walking coordinates are local browser presentation, while agent destinations and cabinet occupancy come from the server. Fifteen games currently use one emulator family, PyBoy for Game Boy/Game Boy Color. Shared visitor presence, verified achievements, general game-playing competence, an uptime SLA and commercial production adoption are outside this baseline.

Next measurements

Measure admission-to-reply latency with unanswered requests kept in the denominator; provider errors and budget pauses by reason; action timing alongside emulated time; stream age under slow clients; and save/restore behavior after injected failure. Define thresholds from a representative baseline before publishing service-level objectives or performance claims.

Technical specification

AH-SPEC-001 · Current control and delivery contracts · September 9, 2026

This specification describes the implemented boundary in arcade/server.py, engine.py, inbox.py, public.py and the MCP tools. The examples are illustrative protocol examples, not extracted production requests.

Authority and routes

Caller Interface Authorized effect
Visitor Public GET /arcade/api/state, GET /arcade/api/frame, GET /arcade/api/residents, WS /arcade/api/live/{resident} Observe public state and actual game frames.
Visitor Public POST /arcade/api/messages?resident=moss Submit bounded public text with a stable request ID.
Resident MCP host Private /control/* with resident bearer credential and lease Observe, act, select games, move and communicate as that resident.
Manager MCP host Private /manager/observe, /goal, /mode Inspect catalog/state and update versioned goals or desired modes.
Supervisor Private /supervisor/tick, /reserve with manager authority Hold exclusive lifecycle lease and reserve scheduled work.

Private routes listen on loopback and are not forwarded by the public edge. The manager and supervisor are distinct responsibilities but share the manager credential; they are not fully separate credential domains. A visitor's Origin header is an admission check, not human authentication.

Action request and commit boundary

A controller first claims a lease and begins a bounded turn. A single action then carries:

{
  "owner": "example-controller",
  "epoch": 12,
  "action_id": "example-action-0042",
  "expected_seq": 41,
  "buttons": ["right", "a"],
  "frames": 12
}

epoch fences an old controller. expected_seq rejects a decision based on obsolete command state. action_id and a payload hash recognize a repeated submission. None is interchangeable with the others: a valid controller can still submit a stale action, and a retry can arrive after the original response was lost.

An input permits zero to four buttons from the eight Game Boy controls and 1–120 frames. Opposing directions are rejected. The engine checks authority, prior receipt, sequence and remaining turn budget under the core lock. A recognized duplicate returns its existing receipt without consuming another action. Execution produces a checkpoint and PNG; these commit with the action receipt and budget change. A failure before commit restores the preceding checkpoint.

Intermediate stream frames may already have reached viewers. They are provisional presentation, not evidence of a completed transaction. The command sequence also advances for a game switch; it is not a measure of player skill or a count of emulator frames.

Short plans and uncertain results

act_sequence accepts a stable plan_id, expected sequence and 1–6 input steps totaling at most 360 frames. Validation occurs before execution; each step has a derived action ID and a separate transaction. If a pause interrupts the second step after the first commits, the first step remains committed. The response reports partial completion for expected interruption.

After a transport failure, the caller may not know which steps committed. Reuse the plan ID for the same plan and inspect returned receipts/state. Do not create a new ID merely because the previous response timed out. This is bounded idempotent execution at the gateway, not exactly-once inference across provider and network boundaries.

Errors have different recovery paths

Condition Boundary result Correct response
Wrong control credential 401 from private gateway Repair operator configuration; do not retry as another resident.
Wrong public origin 403 Reject submission.
Invalid shape, range or forbidden extra field 422 schema rejection; engine semantic validation can return 400 Correct the request from the documented contract.
Stale lease, sequence, occupied cabinet or exhausted allowance 409 conflict Observe the relevant state; renew/reconcile or wait. A conflict is not a generic transient failure.
Public admission cooldown / inbox capacity 429 Wait; preserve the request ID if delivery is uncertain.
Public body exceeds 2 KiB 413 Shorten the request.
Private gateway unreachable through edge 503 Treat delivery as unconfirmed; retry the same logical request.

Visitor request state machine

A browser generates a stable request ID and sends at most 500 characters. Admission stores the public message, inbox entry and request mapping. The request is pending until a resident explicitly replies or dismisses it. Reading an inbox does not imply a response. Reply stores public speech and the resolution together; repeat resolution is idempotent, and controller fencing still applies.

Public bubbles show explicit speech events. Native model reasoning and raw ACP transcripts are not exposed. Peer conversation is a separate addressed queue with recipient-specific acknowledgement, a sender cooldown, daily cap and bounded reply depth.

Stream framing and backpressure

Each WebSocket packet is binary: a four-byte big-endian JSON-header length, that UTF-8 JSON header, then PNG bytes. Header metadata identifies the game, frame/command context and phase. The edge consumes one upstream feed per resident and fans out the latest packet to spectators. A slow viewer skips superseded frames; the application does not maintain an unbounded per-viewer playback queue.

The public edge caps concurrent sockets at 96 overall and 12 per client address. Those are configured admission bounds, not demonstrated sustainable capacity. Nominal 60Hz emulation and up-to-15Hz delivery are scheduling targets, not measured end-to-end frame-rate guarantees.

Architecture decision records

AH-ADR series · Retrospective rationale for the implemented design

These records explain present choices and alternatives. They were assembled from the implementation and operating evidence after construction; they do not imply a historical RFC vote, PDR/CDR signoff or independent reviewer approval.

ADR-01 — Put deterministic control between model and emulator

Context. Model calls can repeat, arrive late or request an invalid action. A conversational instruction cannot enforce exclusive ownership of mutable game state.

Decision. Give the resident scoped MCP capabilities backed by a gateway with credentials, leases, epochs, sequence checks, input bounds and idempotency. Keep host commands outside the resident toolset. Separate public ingress from private controls.

Alternative considered in this retrospective. A CLI with broad shell access could manipulate emulator files directly. It would reduce adapter work but make input authority, recovery and public interaction harder to constrain and audit.

Consequence. More protocol and test code, but a request has an explicit commit point and failure behavior. The manager can direct work without implementing execution safety in prose. The worker still requires outbound provider access; this is not an arbitrary-code network sandbox.

Revisit when. A new emulator cannot meet the current screenshot/input/save contract, or additional capabilities require a more granular permission model.

ADR-02 — Keep each resident's checkpoint transaction local

Context. A screen, save and action receipt that disagree would mislead both the agent and spectator. The current workload has three active cores on one host.

Decision. Use one SQLite WAL database per resident, synchronous FULL, an OS ownership lock and a per-core threading lock. Commit checkpoint, PNG, receipt and allowance update together. Keep manager and peer coordination in separate stores.

Alternative. A shared distributed database plus blob/object storage would support different scaling and failure domains, but the screen/save/receipt boundary would cross more components and need explicit cross-store recovery.

Consequence. Straightforward local transactions and replay checks, with a single-host failure domain and considerable serialization/I/O work. Same-host backups are not disaster recovery.

Revisit when. Measured checkpoint cost becomes material, multiple engine hosts are required, or recovery-point/recovery-time objectives exceed the current deployment.

ADR-03 — Separate decision time, game time and delivery time

Context. Model latency can be seconds or longer. A game that only advances when the model answers gives a misleading impression of real-time play; replaying old frames to fill a gap also misrepresents the environment.

Decision. Advance neutral game ticks while an active lease owns a cabinet. Serialize input actions with that clock. Deliver latest PNG frames independently. Permit bounded input sequences for predictable short maneuvers.

Consequence. The world is live, but fast platformers expose reaction latency. Two predicted inputs save a round trip while increasing open-loop risk. More fluent-looking motion is not evidence of stronger control.

Revisit when. An evaluated low-latency controller can sit below the strategic agent. Compare progress, action latency, failures and observation freshness before claiming improvement.

ADR-04 — Let the manager choose goals; let code own lifecycle

Context. Persistent identities do not need unbounded model turns. Peer replies and process creation can otherwise create uncontrolled recursion or restart storms.

Decision. The manager sets versioned goals and desired modes. The supervisor owns bounded launches, leases, retries and durable work reservations. Peer messages are consumed during normal turns with cooldown and reply-depth limits.

Consequence. Work can stop while the website remains healthy. Launch reservations charge before execution, including unused work after failure. The allowance is not a token or dollar ledger.

Operating evidence. On September 9, the rolling ledger reached 720 resident work units and 48 manager reviews. Resetting provider usage did not clear this independent limit. An authorized recovery backed up the manager database, archived 168 launch reservations and reset the active allowance. Fresh actions followed with sessions and saves preserved.

Revisit next. Expose pause reasons and next eligibility time to operators and visitors; distinguish quota failures from session/tool failures; add a measured spending policy before increasing unattended capacity. Removing the cap would hide the design problem.

ADR-05 — Move lifecycle to a dedicated VM worker

Context. A public arcade depending on a sleeping laptop does not have a dependable operating model.

Decision. Run Codex, ACP, MCP and the supervisor under a dedicated unprivileged VM account. Keep native identity/authentication state separate from source and public assets. Use pinned toolchains and systemd process-group ownership. Model inference remains external at OpenAI.

Evidence and consequence. A one-request Linux probe passed, but four concurrent code-mode hosts exceeded the initial task allowance. Capacity was adjusted and the full workload rechecked. All resident identities resumed; real manager calls were counted only after history replay. This illustrates why startup success, authenticated access and sustained worker operation require different checks.

Revisit when. Provider authentication, process resource growth, incident recovery or shared-host contention warrants a separate worker host. A VM migration removes the Mac dependency, not every availability dependency.

Data model and consistency

AH-DATA-001 · Physical stores and application-enforced relationships

The deployed world uses three resident SQLite databases, one manager database and one peer-message database. Native Codex session history is a separate persistence layer in the private worker home. A resumed model identity does not prove an emulator checkpoint restored, and restoring a game does not prove its controller is healthy.

After Hours data stores and logical relationships

The diagram shows the important logical relationships. The current DDL does not declare foreign keys for these links; application transactions and tests enforce them. Resident ownership is primarily a database-file boundary, not a resident_id column on every row. The full schema is in the source archive.

Resident store: state, authority and delivery

Tables Important keys / fields Purpose and invariant
current Singleton id=1; seq, frames, rom_hash, core, state/PNG blobs One durable active checkpoint. Hash and core identify save compatibility.
actions action_id primary key; payload hash, sequence, buttons, frames, time Recognize a repeated action and reject ID reuse with a different payload.
input_plans plan_id primary key; payload hash Bind a multi-step retry to the original plan; steps commit independently.
control, budget Singleton rows; epoch/owner/expiry/pause and remaining actions Fence stale ownership and bound each turn.
game_saves game_id primary key; hash/core, frames, state/PNG Park one personal checkpoint per game when switching.
game_choices Request ID primary key; target game, serialized receipt Make cabinet-selection retries inspectable.
messages, inbox Message ID; speaker/text/time/kind; status/reply ID Separate public transcript content from delivery state.
message_requests, message_limits Request ID with payload hash/message ID; client digest Deduplicate admission and apply cooldown.
world, world_events Current destination; event ID/time Preserve destinations and recent movements. Browser paths are presentation.
clips, meta Latest clip; keyed resident status Support observation and activity display without exposing native transcripts.

What crosses a transaction boundary

An accepted input updates current, inserts its actions record and reduces budget in one SQLite transaction. Clock ticks also checkpoint under the same core lock. They do not create model action receipts. A switch parks the outgoing game, restores or initializes the target game, and commits the new active checkpoint and choice receipt.

Admission writes the visitor message, pending inbox state and request mapping together. A reply writes public response text and resolves the inbox entry together. These are local database guarantees. There is no transaction spanning provider inference, peer database, resident database and browser delivery.

Manager and peer stores

The manager store has goals and desired rows keyed by resident. Revisions support compare-and-set updates. decisions binds a goal-change request ID to its payload and receipt. supervisor holds the renewable lifecycle lease; launches records reserved work with kind, cost and time. The operator recovery added a private audit table for the one-time launch-ledger reset; it is an operational event, not a normal manager capability.

The peer store's peer_messages table contains a unique request ID, payload, sender, recipient, text, timestamp, reply target, hop count and read flag. Sender identity is derived from authenticated control. Reply targets and depth are checked in application code. Acknowledgement means consumed, not answered or accepted as true.

Schema evolution and recovery implications

Startup creates missing tables and applies the inbox's startup migration logic. There is no general versioned database migration framework. A changed ROM or emulator core must fail closed rather than silently loading an incompatible save. Before an upgrade, test with a copied checkpoint and retain a compatible source/dependency release.

SQLite online backups preserve a consistent database view. Copying a live database file without its WAL is insufficient. The daily job checks five copied databases and retains seven snapshots on the same host; it does not back up provider authentication or all native history. Off-host restore practice and complete retention policy remain open work.

The cadence release adds manager lifecycle rows keyed by subject, recording last launch, next eligibility, wake expiry, failure category and failure count. launch_subjects binds new reservation IDs to their subject so cross-resident ID reuse conflicts. Both are additive tables; existing launch reservations retain their original costs.

Verification and release readiness

AH-TEST-001 · Evidence-linked test plan and current limitations

The goal is to verify authority, consistency and recovery under failure, then inspect the public experience. Emulator compatibility, model access, model competence and service availability are separate questions. One successful model answer cannot establish all four.

Verification matrix

Requirement / risk Test or check in the source Expected result Recorded evidence
R2: interrupted commit test_engine.py::test_failure_rolls_back_core_and_database Previous checkpoint and screen survive injected failure. Passed.
R2: duplicate execution test_duplicate_receipt_does_not_advance Identical retry spends no additional action and does not move the game. Passed.
R3: stale ownership test_stale_controller_and_frame_rejected; test_old_runner_cannot_release_new_controller Old epoch/sequence cannot mutate current control. Passed.
R1: provisional frames test_live.py::test_stream_arrives_before_action_commits_and_recovers_on_failure Viewer can receive provisional frames, then see recovery to committed state. Passed.
R5: fifteen-game saves test_catalog.py::test_every_game_accepts_controls_and_restores_its_own_checkpoint Every installed game accepts controller input and restores its exact parked frame. Passed for 15 entries.
R3/R5: cabinet conflict test_residents.py::test_any_resident_can_switch_to_a_free_game_and_restore_own_save Available game can be chosen; occupancy and personal state remain separate. Passed.
R4: uncertain chat delivery test_inbox.py::test_request_retry_survives_restart_and_rate_limit; test_reply_is_atomic_idempotent_and_fenced Retry maps to the same message; reply resolves it once under valid authority. Passed.
R8: bounded lifecycle test_manager.py::test_scheduling_restart_budget_and_duplicate Schedule and allowance persist across restart; duplicate reservations do not spend again. Passed; exhaustion also occurred in operation.
R3: public boundary test_public.py; public negative-route/origin/body checks No public control route, cross-origin submission or oversized message is admitted. Seven public negative checks passed.
R7: browser usability Chrome desktop, 390px mobile, reduced motion, cabinet dialogs, Escape and touch Fifteen dialogs work, no horizontal overflow, touch moves the visitor, streams render. Recorded browser checks and screenshots.

Names without a filename in the second row refer to the preceding test module. The source archive contains the actual assertions; this matrix is a reading guide, not a substitute for them.

Reproduce the automated checks

.venv/bin/python -m unittest discover -s tests -p 'test_*.py' -q
node --test tests/*.test.mjs

The September 9 VM-world release passed 35 Python tests locally and under the server's Python 3.10 interpreter, plus 16 JavaScript tests. The portfolio passed 22 checks. These counts are a dated release record, not a live status badge. Dependency lockfiles and the source snapshot identify what was tested.

The per-ROM compatibility probe booted each title, exercised generic inputs and compared deterministic output after save/restore. A changing frame after input alone does not prove that input caused useful progress. These checks do not establish every level, control or game objective.

Public and model evidence

The release check requested 17 HTTPS routes, verified three binary PNG WebSocket streams and validated the source archive's 104 file hashes. A browser visitor requested uCity; Moss replied and later selected that game. June selected Flooder. After a subsequent usage recovery, all three residents produced new input receipts while their original native sessions and game saves remained intact.

Inspect release verification, public checks, browser checks, game compatibility and selected action receipts. Each artifact is a snapshot. Handbook publication checks record the current document links, downloads and source-archive hash after the README correction. Public speech is a model report; the system does not independently verify the reported milestone.

Release gates and rollback

A release should pass the pinned-runtime tests, match its artifact manifest, keep private data outside the archive, preserve a compatible checkpoint and prior release, and then demonstrate real control and chat over public HTTPS. Service status alone is insufficient. A model-ready event is also insufficient: session loading can replay old tool calls, so evidence must come from the new turn.

Deploy only the scoped arcade/portfolio artifacts. Stop the worker before replacing engine code, retain private state, restart through the existing supervisor contract, and allow the prior lease to expire. If new control fails, restore the compatible source symlinks and dependency versions with state preserved. Database corruption requires a tested backup, not repeated restarts. Detailed procedures are in the architecture and operations guide.

Prioritized follow-up work

Priority Work Acceptance criterion / dependency
P0 Explain pauses and quota recovery Public status distinguishes provider failure, local allowance exhaustion, operator pause and disconnected control; operator sees next eligibility time. Requires structured failure reporting.
P0 Test session/provider failure recovery Simulated quota/auth/tool-host failures produce bounded retries and honest inactive state; successful recovery produces a new real action without losing identity.
P1 Measure latency and work cost Record decision/action/reply latency, failed and unused reservations, and available provider usage. Keep no private reasoning in public telemetry. Establish a baseline before choosing targets.
P1 Practice off-host restore and retention Restore five world databases plus separately managed native state on a clean authorized host; document measured loss/recovery and deletion policy.
P1 Evaluate control quality Use fixed games, saved starting states, repeated trials and explicit progress criteria. Compare two-step planning with one-step observation; report failures and uncertainty.
P2 Broaden accessibility and abuse testing Keyboard-only and screen-reader review, malicious message scenarios, long sessions, slow clients and concurrent spectators. Define safety/load claims only from results.

There is no SOC 2, GDPR or HIPAA certification claim, no sustained-load or uptime proof, and no completed-game benchmark. The security discussion describes controls and residual risks; it is not a compliance attestation.

Subscription-conserving lifecycle verification

The cadence release adds a simulated 24-hour quiet-room test (48 prompt turns per resident, 24 manager reviews), presence wake-up tests, restart and rolling-expiry tests, and checks that provider backoff and operator pause outrank visitor activity. Counts are prompt turns, not exact subscription tokens. A public visit activates a bounded two-minute cadence; quiet periods use a thirty-minute resident heartbeat. All viewers share the same residents.