Server sessions
Last updated
May be stale
This page was last updated ago, on 3 September 2026. Check what it says against the code before relying on it.
The premises every ticket under #3431 inherits, settled in a charting session on 2026-09-01. This is the folder's landing page; it is not the spec.
Read the spec first. Charting is now finished, and three of the premises below were superseded by the tickets that tested them — each is marked inline. The decisions as they actually stand are ADR 0001 (what a session is, and why) and Phase one (what to build). This page is kept as the record of where the route started.
| Document | What it is |
|---|---|
| ADR 0001 | The decision, as it stands |
| Phase one | The buildable spec — library, container_code, track |
| Problem statement | The original note, with the two premises charting corrected |
| Research: the boot predicate | #3432 — is GM:Initialize the right derivation point? |
| Research: FearlessRP prior art | #3433 — operational edge cases worth carrying |
| Research: other reinventions | #3434 — who else rolled their own boot identity |
| Research: the table-name config keys | #3435 — is the FK declarable? |
Research: DB_MAIN column typing |
#3442 — what Lua type does a result column arrive as? |
What a session is
A world-load lifetime, not a process lifetime. This corrects the problem statement's own
wording. refund_ents — the consumer with a live exploit behind it — cares about the world being
wiped, and a changelevel keeps the process and Lua state alive while destroying every entity. A
PID-based marker would therefore newly break the changelevel case that today's
os.time()-at-file-load happens to get right.
A distinct processId is exposed alongside it where cheap, for the genuinely process-scoped
consumers the problem statement also lists — uptime and "last restarted at". Conflating the two is
how this bug was made in the first place, so they stay named apart.
Identity
Superseded. #3438 split identity into an internal session key (the
int unsignedautoincrement — every FK, every lookup) and an external session key (a 64-char token minted in MySQL, held as a credential, never a join key). #3437 then retired the synchronous-availability premise below: with the autoincrement as the key and the token minted server-side, nothing a consumer needs exists synchronously, and fail-closed became the API rather than a safety net over it. The section is kept as written.
A locally generated token, plus the DB row's autoincrement for ordering.
Libraries load from sh_init.lua's includeCore("libraries") pass — before any plugin, and long
before the database connects. FearlessRP's prior art sidesteps this by living in a plugin and
hooking DatabaseConnected; a library cannot. So synchronous availability at load is
non-negotiable, which rules out a DB autoincrement as the identity. The autoincrement is kept
anyway, because "last session" lookups and container_code's ORDER BY ... DESC LIMIT 1 need
something orderable.
The row carries LLServer and the raw map string as separate columns. The existing server_stats
matches map = <LLServer> — a server code compared against a column named map — and that
ambiguity does not survive.
Consequences
previous()is cut from the API.refund_ents'lastSessionis not a crash signal; it is a scoping window (start_time = lastSession OR start_time = session,refund_ents/sv_init.lua:165-170) that silently drops contraband for anyone who misses two restarts. Refunding onsession_id ~= currentremoves the API, removes theMAX(start_time)query inOnDBConnected, and fixes that.- Crash-versus-clean is irrelevant to
refund_ents. Clean shutdown,changeleveland crash all wipe the world; how the previous session ended never changes the outcome. (Right as a design goal, wrong as a description of today: #3436 found Livetrackholds 12,898 rows and 18 unflagged ones across 2 of 356 sessions, because theCallOnRemoveflags everything at shutdown teardown — so only crashes leave anything claimable.IsClosing()is what makes the premise true.) The heartbeat ships anyway, justified solely by player-session playtime integrity in phase two — a crash otherwise leaves every player row open forever — and is never read back to trigger a reboot. - Fail closed.
cityrp.sessionrefuses to serve until its DB row lands, andrefund_entsearly-returns fromTrackwhile not ready, matching its existingLLServer == "build"guard. This follows from taking a foreign key: an orphanedtrackrow is not a reporting gap, it is permanently unclaimable player property. A DB outage must stop new tracking loudly rather than silently mint property nobody can reclaim. - The configurable table-name keys die. A numbered migration cannot declare an FK against a
table a server can rename, so
MySQL Entity Tracker TableandMySQL Online Log Tableare hardcoded. #3435 verified this is safe. - Auto-refund is replaced entirely by a client-initiated "lost and found" claim window, so
players reclaim at their leisure rather than receiving a lump grant on login. Entity
re-ownership — the
session_id = currenthalf — stays automatic and separate. One grant path over the table, not two; two is how the cross-player mixing bug survived five years. server_statsis superseded, migratingcontainer_codeoff itsORDER BY session_id DESC LIMIT 1boot heuristic, which is itself a second instance of the reinvention this whole effort exists to stop.- The library is server-only, at
gamemode/core/libraries/sv_session.lua. Nothing in the use-case list is client-side, and a networked session id would be a net message designed for no current consumer.
Phasing
Phase one — the library, container_code, refund_ents — is the exploit fix and must stay
shippable alone.
Phase two, player sessions, replaces the onlinelog plugin with a player_sessions ledger
keyed on the new session id, dual-writing both tables during a cutover window so the internal tools
reading onlinelog can migrate on their own schedule. One row per connection, plus a
player_session_jobs child table of job intervals — rather than FearlessRP's
close-and-reopen-per-team-change, which overloads "session" into a third meaning. It is explicitly
severable: abandoning it does not invalidate phase one.
Phase three, the lost and found, is the claim window that replaces auto-refund. It has its own
design tree — expiry, spawn-versus-inventory, anti-abuse, cityrp.ui scaling — and shares exactly
one decision with phase one: what counts as lost.
Out of scope
- The remote restart kill-switch. FearlessRP's heartbeat reads
closed_atback and self-changelevels, so anyone who canUPDATE server_sessionscan reboot a live server under full load. Useful, but a separate feature: coupling it to the ledger means a logging bug can restart Live. Thelast_heartbeat_atcolumn phase one does ship makes it a pure follow-up rather than a schema change. - A query builder for CityRP2. The prior art depends on one; the session tables are
re-implemented natively against
DB_MAIN:RunCheckedinstead — notRun, which dispatches its callback even when MySQL errored (#3433). - Migrating the internal tools off
onlinelog. The phase-two dual-write window exists to buy them time.