Research: other boot-identity reinventions
Last updated
May be stale
This page was last updated ago, on 2 September 2026. Check what it says against the code before relying on it.
- Ticket: limelight-development/cityrp2#3434
- Parent map: #3431 — spec for a server-only
cityrp.sessionlibrary atgamemode/core/libraries/sv_session.lua. - Date: 2026-09-01
- Status: Research complete. Planning only — no gamemode code was changed.
Repo citations are path:line relative to gamemodes/cityrp_2_by_limelightgaming/ unless the path
starts addons/, docs/, lua/, or is AGENTS.md. Every hit below was confirmed by reading the
surrounding code, not by trusting a grep line. Engine claims are quoted from
wiki.facepunch.com/gmod and nothing else.
Sibling research: research-gm-initialize-boot-predicate.md
(#3432, the predicate) and research-fearlessrp-sessions-edge-cases.md
(#3433, the operational edges).
TL;DR
Boot identity has been reinvented at least six independent times, in four mutually
incompatible representations, and no two of them agree on what a boot is. The two the map
already knew about are the tip: the gamemode currently carries a Lua epoch integer
(refund_ents), a MySQL autoincrement reached through a correlated subquery (container_code via
server_stats), a settings key-value row read by an out-of-repo service (db.lua), and a
one-second-heartbeat file on disk (sv_logs) — four separate stores, all recording "the server
started", all keyed differently, all written from a different point in the boot, none of them aware
of the others.
One further shape matters and is not a store at all: DBConnected used as "everything before now
is void", which in giveprops deletes rows belonging to other live servers because the boot has
no identity to scope the delete by.
| # | Site | Representation of "this boot" | Wrong today? | Recommendation |
|---|---|---|---|---|
| 1 | refund_ents/sv_init.lua:21 |
Lua os.time() at plugin file scope |
Yes — unguarded; a re-include re-derives it | Migrate. Primary consumer, already named by the map. |
| 2 | container_code/sv_dbcode.lua:14,61 + init.lua:2978 + server_stats |
MAX(session_id) via correlated subquery, resolved at query time |
Yes — the producing INSERT is a timer.Simple at init.lua file scope |
Migrate. Retire server_stats; see the duplication case in §2. |
| 3 | db.lua:72 (settings.last_start_<server>) |
os.time() in a key-value row, written per DB connect |
No — but its only reader is out of repo (the changelogs service) | Keep writing it. An undeclared external contract; see §3. |
| 4 | sv_logs.lua:121-126, 133-135 |
A _time.txt heartbeat file rewritten every second |
Yes — the read/append is at core-library file scope | Migrate. Wants startTime + the previous session's close-out. |
| 5 | sv_logs.lua:195-204 (local first = true) |
A file-local boolean "have I logged the start yet" | Yes — a re-include re-arms it | Migrate. Exactly the announced guard #3432 sketched. |
| 6 | sv_commands.lua:3109-3115 + discord_relay_free/sv_init.lua:279-281 |
CurTime() reported as uptime |
n/a — not a hit | Excluded. An uptime readout, not a boot sentinel; see §6. |
| 7 | giveprops/sv_init.lua:108-111 |
DBConnected as "wipe everything older than this boot" |
Yes — unscoped DELETE, hits other live servers |
Migrate (scope only) + raise a Bug (§7). |
| 8 | sv_auto_restart.lua:96-101 |
CurTime() as world-load uptime |
No | Leave alone. Correct for its own intent; optional tidy-up. |
Six implementations across five files, then. Three further sites (carboosting, daynight,
spawnpoints) ask a different question —
"am I in a boot or a hot reload?" — and answer it correctly today. They are not phase-one
consumers, but they are the strongest evidence for the ADR's rule; see §9.
1. refund_ents — a Lua epoch integer at plugin file scope
refund_ents/sv_init.lua:21-23:
PLUGIN.session = os.time()
PLUGIN.lastSession = 0
PLUGIN.server = LLServer
Predicate actually wanted: world load. The token is written as the start_time column of every
tracked row and read back in PLUGIN:Load (sv_init.lua:165-171), which selects rows matching
either self.lastSession or self.session and then splits them at sv_init.lua:179:
local current = row.start_time == self.session
current rows get their ownership re-attached to the still-standing entity; everything else is
refunded to inventory (sv_init.lua:193-210). lastSession is resolved on DBConnected by
SELECT MAX(start_time) … WHERE server = … AND start_time < self.session (sv_init.lua:234-241) —
the only MAX()-over-a-boot-stamped-table in the gamemode.
Wrong today. Two ways, both already charted by #3432 and repeated here for completeness:
- The derivation is unguarded.
luarun,sincandlua_refresh_fileall re-execute a single file's scope without re-runninggamemode/sh_init.lua:11(cityrp = {}), so a hot reload of this plugin mints a new session while every tracked entity is still physically standing. The nextPLUGIN:Loadclassifies all of them as wiped and refunds them — the item is now in the player's inventory and on the ground. row.start_time == self.sessioncompares a DB result value to a Luanumberwith==(sv_init.lua:179). If MySQL marshals that column as a string the comparison isfalsefor every row and the same duplication follows. Unverified against a live DB; carried forward from #3432 §5(b).
Recommendation: migrate. This is the consumer the map already names, and the migration is smaller than "adopt a hook" — file scope is already the right predicate, it just needs to be the library's guarded file scope rather than the plugin's unguarded one.
2. container_code — a MySQL autoincrement reached by correlated subquery
Three files and a table, none of which know about each other.
The producer — gamemode/init.lua:2977-2980:
-- Log server start for container system
timer.Simple(10, function()
DB_MAIN:Run("INSERT INTO server_stats VALUES(NULL, NOW(), '".. LLServer .. "')")
end)
The table — gamemode/core/migrations/0001_baseline_live_schema.lua:770-775:
CREATE TABLE IF NOT EXISTS `server_stats` (
`session_id` int NOT NULL AUTO_INCREMENT,
`startup_time` datetime NOT NULL,
`map` varchar(30) NULL,
PRIMARY KEY (`session_id`)
) ENGINE=InnoDB CHARSET=latin1
Note the positional VALUES(NULL, NOW(), LLServer): the column named map holds the server key
(v2d, v4b1, …), not a map name. Every reader below therefore filters WHERE map = "<LLServer>".
The consumers — container_code/sv_dbcode.lua:14 (read) and :61 (write):
container_id != (SELECT session_id FROM server_stats WHERE map = "]] .. LLServer .. [[" ORDER BY session_id DESC LIMIT 1)
(SELECT session_id FROM server_stats WHERE map = "]] .. LLServer ..[[" ORDER BY session_id DESC LIMIT 1),
A 30-second timer (sv_dbcode.lua:50-70) snapshots every cityrp_container's contents into
crash_container, stamping the row with the current boot id; /c crashed
(sv_command_c.lua:100-103 → checkCrashContainerDatabase, sv_dbcode.lua:2-47) hands back the
contents of any row not stamped with the current boot.
Predicate actually wanted: world load — "is this snapshot from a run of the world that is now
gone, or from the one I am standing in?" Identical semantics to refund_ents, expressed in a
completely different medium.
Wrong today — the concrete failure case. The boot id is not captured at boot; it is
re-resolved on every query from whatever the newest server_stats row happens to be. The row that
defines it is produced by a bare timer.Simple(10, …) sitting at file scope of gamemode/init.lua.
Any re-execution of that file's scope — luarun, sinc, lua_refresh_file, i.e. exactly the
vectors #3432 §2 enumerates — arms a second timer and inserts a second server_stats row ten
seconds later. The boot identity has now advanced with no restart and no changelevel. Every
crash_container row written before that moment is instantly "from a previous session", so every
container in the world becomes eligible for /c crashed restore while the physical container is
still standing in front of the player holding the same items. That is the refund_ents
duplication bug in a second plugin, reachable by a different lever.
Two lesser problems in the same code, worth recording because a migration will touch them:
- The restore is dead.
sv_dbcode.lua:30-33iterates the recovered items with the only inventory write commented out (//cityrp.inventory.update(self, k, v)) and then tells the player"Items have been placed in inventory". Nothing is granted. Out of scope for #3431 — flagged in §10. res[1]is dereferenced unguarded.sv_dbcode.lua:16-17readsres[1]["json"]with no emptiness check, so/c crashedwith nothing to restore errors inside the callback. Also §10.
Recommendation: migrate, and retire server_stats with it. server_stats exists solely to
serve this subquery — init.lua:2978 is its only writer and sv_dbcode.lua:14,61 its only readers,
repo-wide. Once crash_container rows carry the session's own identity, the table, the delayed
timer, and the correlated subquery all go away together.
3. db.lua — a settings row with an out-of-repo reader
gamemode/db.lua:71-72, inside the successful-connect path:
local time = os.time()
self:FRun("INSERT INTO %s (setting, value) VALUES ('%s', %s) ON DUPLICATE KEY UPDATE value = %s", cityrpserver["MySQL Settings Table"], "last_start_" .. self.cfg["Server"], time, time)
Predicate actually wanted: "when did this server last come up". Overwritten in place, so it is a
latest-boot marker with no history — closer to processId in spirit than to a session id, though
it is re-written on every changelevel too, so it is neither cleanly.
Not dead, and not this repo's to delete. A repo-wide grep for last_start returns exactly this
one line — but the grep only covers this repository. The row is consumed by the changelogs
service (limelight/services/changelogs, in WebEnv), which reads last_start_<server> to know
when each server last came up. The write is a live cross-service contract that happens to be
invisible from here.
Recommendation: keep writing it, and say so in the ADR. cityrp.session may take ownership of
the write — it is the natural owner of "this server just came up" — but the key name
(last_start_<server>), its epoch-seconds value, its home in the settings table and its
overwrite-in-place semantics are all fixed by a consumer outside this repository. Renaming,
retyping or dropping it breaks changelogs silently, with the failure surfacing on a web dashboard
rather than in any GMod console.
This is the sweep's own near-miss, and worth recording as one. "Grep found no readers" is not "there are no readers" in a multi-repo estate: three of this sweep's absence claims were repo-scoped greps, and this is the one that had a consumer on the other side of the boundary. The ADR's consumer list should be explicit that cross-repo readers exist for the session-adjacent data, so the phase-one migration does not delete another one.
4. sv_logs — a one-second heartbeat file on disk
gamemode/core/libraries/sv_logs.lua:121-126, at core-library file scope:
local ctime = file.Read("cityrp/"..LLServer.."_time.txt");
ctime = tonumber(ctime)
if ctime then
file.Append("cityrp/"..LLServer.."_prop_log.txt", os.date("%d/%m/%Y - %H:%M:%S", ctime).." ------------ Server Stop ------------\n");
end
file.Append("cityrp/"..LLServer.."_prop_log.txt", os.date("%d/%m/%Y - %H:%M:%S").." ------------ Server Start ------------\n");
fed by sv_logs.lua:133-135:
timer.Create("crashtimetracking", 1, 0, function()
file.Write("cityrp/" .. LLServer .. "_time.txt", os.time());
end)
Predicate actually wanted: the previous boot's end instant. The timer name says it outright —
crashtimetracking. GM:ShutDown cannot record the stop time of a server that crashed, so this
writes the wall clock once a second and lets the next boot read the last surviving value back as
"when the previous run died". It is a hand-rolled last_heartbeat_at, which is precisely the column
#3433 §3 specifies on server_sessions.
Wrong today. Three ways:
- The read-and-append is at file scope of a core library, so it is subject to the same reload
vectors as everything else in #3432 §2. A
lua_refresh_fileonsv_logs.luaappends aServer Stopline dated one second ago followed by aServer Startline dated now — a restart that never happened, written into the file admins read to reconstruct incidents. The heartbeat timer is meanwhile still running, so the "stop" it reports is a live server. - It cannot see a
changelevel. The marker is keyed onLLServerand nothing else, so a map reload — whichsv_auto_restart.lua:93performs on a timer and!map/!restarton demand — produces aServer Stop/Server Startpair indistinguishable from a genuine process restart. The distinction the whole of #3431 is built on is invisible here. - It is a fourth, parallel store. A crash on a server that has been up for hours leaves
server_stats(§2),settings.last_start_*(§3),refund_ents.start_time(§1) and this file all holding different, unreconcilable answers about the same boot.
Recommendation: migrate. The prop log's Server Stop line wants the previous session row's
last_heartbeat_at; the Server Start line wants this session's opened_at. Both are already in
the #3433 schema, and moving the heartbeat onto the session library removes a per-second disk write
in the bargain.
5. sv_logs — a file-local first flag as a once-per-boot guard
gamemode/core/libraries/sv_logs.lua:195-204:
local first = true
hook.Add("DBConnected", "LogRestart", function()
cityrp.logs.add(nil, nil, nil, nil, "debug", "dbconnected")
if first then
cityrp.logs.add(nil, nil, nil, nil, "start")
…
first = false
end
end)
Predicate actually wanted: "is this the first DBConnected of this boot". The dbconnected
debug line is emitted every time; the audit-visible start event only for the first — i.e. the flag
exists specifically because DBConnected can fire more than once per world load (a reconnect) while
start must mean the world came up.
Wrong today. first is a local at file scope, so a re-include resets it to true. Any hot
reload of sv_logs.lua followed by a DB reconnect writes a second start row for one world load,
into the same audit log operators use to bound "was the server up at the time". This is the exact
trap #3432 §3 warns about — a token in a local is re-derived by every re-include with nothing to
guard it — and the exact boolean case sh_tophints.lua:30 documents needing an == nil guard
rather than an or.
Recommendation: migrate. cityrp.session.announced in the #3432 sketch is this flag, correctly
guarded and hanging off cityrp rather than a file-local. sv_logs should read it instead of
keeping its own.
6. Not a hit: up is an uptime command, not a boot sentinel
Listed here because it looks like a hit under the sweep's grep shapes and is not one, so the next person to run this sweep does not re-raise it.
gamemode/core/sv_commands.lua:3109-3115:
cityrp.acommands.AddCommand("up", function(ply)
ply:Notify(cityrp.formattime(CurTime(), true))
end, 0, "m", false, "moderator",
"<none>",
"Check the server's current uptime."
)
and its Discord twin, discord_relay_free/sv_init.lua:279-281:
if cmd == "up" then
self:rtnCmd(cityrp.formattime(CurTime(), true))
This reads a duration and prints it. Nothing branches on it, nothing stores it, nothing compares
it against a previous boot — it never functions as an identity or a sentinel, which is what the
sweep is looking for. It belongs with the other duration readings in §9, not in the hit list: how
long the current world has been up is a legitimate thing for !up to answer, and CurTime()
answers it.
The changelevel reset noted on the RealTime
wiki page is therefore not a defect here — it is the same reset that makes sv_auto_restart's
reading correct in §8. The same value reaches the audit log at sv_logs.lua:191, :200 and
sv_temar.lua:414 with the same meaning.
Recommendation: leave alone. If processId later makes process uptime cheap to report
alongside world uptime, !up is an obvious place to surface it — but that is a feature request
against the command, not a session-identity consumer and not a migration this map owes anyone. The
duplicated CurTime() literal across two files is a footgun #11 shape worth tidying if either file
is opened for another reason.
7. DBConnected as "everything before now is void" — giveprops
gamemode/core/plugins/giveprops/sv_init.lua:108-111:
-- Every loan is voided on boot, before any player can reconnect.
hook.Add("DBConnected", "GivePropsWipeLoansOnBoot", function()
DB_MAIN:Run("DELETE FROM giveprops_loans")
end)
Predicate actually wanted: "rows that do not belong to my session". The table is a
reconnect-recovery cache for in-memory prop-limit loans (sv_init.lua:83-141); a loan cannot
outlive the world it was made in, so it is voided at boot. That is the right intent, and unlike §1–§4
it does not roll its own token at all — which is precisely why it goes wrong.
Wrong today. giveprops_loans has no server column
(gamemode/core/migrations/0008_add_giveprops_loans.lua:8-13: the primary key is
(giver_steamid64, recipient_steamid64) and there is nothing else). With no identity to scope by,
the delete is unqualified — so a boot of any Limelight server wipes every other live server's
loans mid-session. Concretely: v4b1 hits its sv_auto_restart window and reloads; on v2d, two
players who are still online and still holding a live loan have their row deleted underneath them.
Their in-memory ledger and cityrp.layers entries survive, so nothing looks wrong until one of them
disconnects and reconnects — loadLoans (sv_init.lua:115-141) then finds no rows, rebuilds
nothing, and the loan silently evaporates: the giver gets their prop limit back, the recipient
loses theirs. No log line records it, because from giveprops's point of view nothing happened.
Recommendation: migrate (scope only), and raise the wipe as its own Bug. The library fix is
one column plus DELETE … WHERE session_id <> cityrp.session.id; the ADR should name this site
because it is the cleanest example of the ADR's own rule — the plugin did not invent a heuristic,
and got a cross-server data-loss bug precisely because there was no shared one to ask.
For contrast, the two neighbouring DBConnected cleanups are not boot-scoped and are correct as
written: policedb/sv_init.lua:359-361 (created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)) and
admin/sv_init.lua:215-217 (chat older than 7 days) are age-based retention that happens to be
triggered at boot. Leave both alone.
8. sv_auto_restart — CurTime() as world-load uptime, correctly
sv_auto_restart.lua:96-101, with time = CurTime() from :62:
if (time > PLUGIN.restartHardTime) and (count <= PLUGIN.restartHardMax) then
PLUGIN.restarting = "Hard"
end
against PLUGIN.restartSoftTime = 14400 and restartHardTime = 28800 (:16, :22).
Predicate actually wanted: uptime of this world load — the thing a map reload is supposed to
refresh. CurTime() resetting on changelevel is the desired behaviour here, not a bug: the
countdown should restart after a reload.
Not wrong. Worth two notes for whoever migrates it later: CurTime is documented as "affected
by various factors such as host_timescale … and the server being paused", which os.time() - cityrp.session.startTime is not; and the plugin stores its countdown state on PLUGIN fields
(:33-40) that a re-include resets, so a hot reload silently cancels a restart in progress.
Recommendation: leave alone for phase one. Name it in the ADR only as an example of a correct world-load reading, so the migration sweep does not mistake it for a bug.
9. Near misses and distinct shapes — and why each is excluded
Per-player, per-connection identity (the onlinelog family)
onlinelog/sv_init.lua:32 sets ply._jointime = os.time() on PlayerInitialized and
:13-29 mints a 10-character alphanumeric token per connection; both go into the online-log row at
:43 and both are used as the row's lookup key on update and disconnect (:55, :62, :67).
This is per-player connection identity, not boot identity, and the two must not be conflated:
_jointime changes many times within one boot and is scoped to a single player. It is the
player_sessions half of the #3433 schema, not the server_sessions half. Excluded — but it does
carry one lesson for the library: it is a second place where an integer epoch is used as a
composite DB key (WHERE … AND connect = '<epoch>'), with the same string/number coercion exposure
noted in §1. If the ADR is going to normalise a token type, this is the shape it is normalising
against.
CurTime()/SysTime() used as durations, not restart signals
derma/cl_notifications.lua (StartTime = SysTime() throughout), login_rewards/cl_init.lua:156,
website_rewards/cl_init.lua:124, xp-bar/cl_init.lua:31,70, maintenance/cl_minigame.lua:47,130,
carboosting/sv_init.lua:4047,4061, db.lua:155,193. All measure an elapsed interval within a
frame budget or a UI animation. None is asking "did the server restart". Excluded.
Terminology collisions that will show up in any future grep
Three places use the word session for something unrelated to a boot, and all three will surface in a naive sweep:
propsave/sv_init.lua:53, 99, 108, 136, 185, 197— "prop session" is a player-initiated manual save of their own props, keyed by SteamID and stored per-LLServeron disk (:24, :155). Not time-derived, not boot-scoped.entities/weapons/cityrp_lockpick/shared.lua:431, 616, 623—sessionis the per-attempt lockpicking minigame state.referrals/sv_session.luaandreferrals/cl_session.lua— filenames only; the contents are the referral UI. This is the one that matters, because the new library's file isgamemode/core/libraries/sv_session.luaand the two will be a permanent source of confusion in file pickers and grep output. Worth a sentence in the ADR.
Boot-time reconciliation that is already correct
db.lua:73, init.lua:2400 and sv_logs.lua:189 all run
UPDATE players SET _Online = 0 WHERE _Server = '<server>' — on connect, on shutdown, and on
GM:ShutDown respectively. This is genuine boot reconciliation of state a crash would have left
stale, and it is the one place the codebase gets this right: the flag is per-server with no
per-boot dimension, so a server key is the correct scope, and sv_player.lua:471-486 documents at
length why the preload path depends on that guarantee. Excluded, and worth citing in the ADR as the
counterexample — scoping by server is right when the state has no boot dimension; §7 is what happens
when it does.
Clock broadcast, not identity
sv_player.lua:932-934 pushes SetGlobalInt("cityrp_servertime", os.time()) once a second so
clients share a wall clock. Not a boot marker. Excluded.
Dead schema
currentstats (migrations/0001_baseline_live_schema.lua:270-275: server, loading, ingame,
admins, adminnames) is a per-server live-status table with no reader and no writer anywhere in
the repository. It is not a boot-identity reinvention, but it is a fifth abandoned attempt at
server-state bookkeeping sitting in the baseline schema, and it belongs in whatever cleanup retires
server_stats. Flagged in §10.
Vendored addon code (not consumers to migrate)
Swept addons/ and lua/ explicitly. No vendored addon carries a boot heuristic of any kind —
there is no server_stats reader, no os.time() - RealTime(), no file-scope boot token outside the
gamemode. The two file-scope os.time() captures that exist are both damagelog
(addons/damagelog/lua/damagelogs/server/oldlogs.lua:11, a log-retention cutoff, and
addons/damagelog/lua/damagelogs/server/recording.lua:164-165, minute-boundary alignment for its
dump timer), and neither is boot identity. addons/pcasino's SetStartRoundIn(os.time())
(lua/perfectcasino/core/sv_core.lua:80,117 and the table entities) is per-round betting-window
state. Stated explicitly rather than silently omitted: none of these should be migrated, and a
vendored addon that grew its own boot heuristic later would still not be a consumer — the whole
point of addons/ staying a clean drop-in replace.
The adjacent question the sweep kept hitting: "am I a boot or a hot reload?"
Three sites ask a related but genuinely different question — not which boot, but whether this file-scope execution is part of one:
carboosting/sv_init.lua:4049-4058— a comment stating thatCityRP.Core.PostPluginLoad(sh_init.lua:217) only fires on a full boot, followed by the same maintenance call made both immediately and from the hook, so a reloaded plugin still gets its fence NPCs. Paired with the entity sweep at:543-552that reconciles the world when the reload loses track of them.daynight/sv_init.lua:323-324—timer.Simple(0, bootstrap)under the comment "Hot reload / late gamemode load: InitPostEntity may have already fired."spawnpoints/sv_init.lua:118-121— a lazy cache fill for the same reason, commented as such.
All three are correct today and none should be migrated in phase one. They are recorded here
because they are the strongest evidence in the codebase that "is this a boot?" is a question plugins
are already asking and answering three different ways — and because cityrp.session can answer it
for free (the library's own token existing before a plugin's file scope runs is the reload signal).
Flagged as fog in §10, not proposed as API.
10. Evidence for the ADR's rule
The ADR wants to say: future systems ask cityrp.session rather than rolling their own. What the
sweep proves, specifically:
-
It gets reinvented roughly once per feature that needs it, not once ever. Six independent implementations across five files (§1–§5, §7):
refund_ents,container_code,db.lua,sv_logs(twice) andgiveprops— written by different authors across a decade of the codebase, none referencing another. -
No two agree on the representation. A Lua epoch integer, a MySQL autoincrement, a key-value settings row, and a text file on disk. Four stores; a crash leaves all four holding different answers to "when did this boot start", and there is no procedure — and no code — that reconciles them. One of the four (§3) is already load-bearing for a service in another repository, and nothing in this one says so.
-
The failure mode is always the same and always silent. Every wrong case in §1, §2, §4, §5 and §7 is a re-execution of file scope that the author did not model: the code compiles, runs, and passes the case that was tested (a real restart), then misbehaves on a hot reload or on another server's restart with no error to read. That is AGENTS.md footgun #7 stated as a measurement rather than a warning.
-
The two worst outcomes are duplication bugs in economy code. §1 and §2 are the same bug — "still-standing thing classified as lost, refunded anyway" — in two plugins, reachable by two different levers. The
refund_entsvariant has real history: the cross-player form of it (todo/server-session-system.md, issue #106) took five years to fix. §2 has not been reported, which is not evidence that it does not fire. -
Not inventing a heuristic is not a defence either. §7 shows what the alternative looks like when no shared source of truth exists:
givepropscorrectly declined to roll its own token, had nothing to scope its cleanup by, and shipped an unqualifiedDELETEthat reaches across the fleet. A library is not just a convenience for the systems that would otherwise reinvent it — it is the only thing that makes the correct scoping expressible. -
server_statsis the counterexample that proves the library needs to be a library. It is, structurally, exactly what #3431 asks for: one DB row per boot, with a server key and a start time, in the baseline schema since forever. It failed anyway, because it has no Lua-side owner — the token is re-resolved by subquery on every read, the producing INSERT is a naked timer atinit.luafile scope, the column names lie about their contents, and no second consumer ever adopted it. A table is not an API.
11. Flagged, not acted on
For the map, or for their own tickets. Nothing below was changed and nothing below is in #3431's scope.
New fog for the map
- "Am I in a boot or a hot reload?" is a second, distinct question the codebase keeps asking
(§9,
carboosting/daynight/spawnpoints).cityrp.sessioncan answer it as a side effect — the token already existing when a plugin's file scope runs is the reload signal — but exposing that (cityrp.session.IsBooting()or similar) is a design decision, not a free consequence, and the three current call sites are correct without it. Deliberately not proposed as phase-one API. - Type discipline on the token. §1 (
row.start_time == self.session) and §9 (onlinelog'sconnect = '<epoch>'composite key) both compare a DB-marshalled value to a Lua number. If the library owns the token it should own its type and offer the comparison, rather than leaving each call site to get coercion right. Carried forward from #3432 §5(b); the sweep found a second instance of the shape.
Candidate new tickets (Bug)
-
giveprops_loansis wiped fleet-wide on any server's boot (§7). Cross-server data loss, independent of #3431 — the row needs a server (or session) column and theDELETEneeds aWHERE. Sized small; the migration is one column. -
/c crashedgrants nothing.container_code/sv_dbcode.lua:30-33— the inventory write is commented out and the player is told the items were restored. Player-facing, live. -
/c crashederrors when there is nothing to restore.container_code/sv_dbcode.lua:16-17dereferencesres[1]["json"]with no emptiness guard. Out of scope / cleanup -
currentstats(migrations/0001:270-275) has no reader and no writer. Retire it alongsideserver_stats. -
settings.last_start_<server>(db.lua:72) must not be cleaned up — its reader is the changelogs service, outside this repo (§3). Worth an ADR line so a future tidy-up does not remove it on the strength of a repo-wide grep. -
sv_logs.lua's one-secondfile.Write(:133-135) is a per-second disk write whose only purpose is theServer Stopline in §4. Migrating §4 removes it; worth noting as a small performance win rather than pure cleanup. -
Naming collision:
gamemode/core/libraries/sv_session.lua(new) vsgamemode/core/plugins/referrals/sv_session.lua(existing, unrelated). Not worth renaming the referrals file, but worth a line in the ADR so the next reader is not misled.
Confidence
Verified from source — every path:line citation, read in this worktree at 06338d9a5. The
absence claims were each checked by an explicit repo-wide grep: last_start (one hit in this repo,
the writer — but see §3: the reader is the changelogs service in another repository, so this
particular absence claim was wrong as first written);
server_stats (one writer, two readers, one schema line); currentstats (schema only); file-scope
os.time() assignment (one hit in the gamemode, refund_ents/sv_init.lua:21; one in vendored
damagelog); MAX( over a boot-stamped table (one hit, refund_ents/sv_init.lua:235);
os.time() - RealTime()/SysTime() (no hits anywhere, gamemode or addons).
Inferred — that the container_code reload path in §2 is live rather than prevented somewhere
unread (the mechanism is confirmed; nobody has reported the bug). That the §7 loss sequence plays
out as described end-to-end; the wipe, the missing column and loadLoans's behaviour on an empty
result are all confirmed in source, but the sequence has not been reproduced on a server.
Unverified — MySQL's marshalling of the start_time column as string vs number (§1), inherited
from #3432 and still needing a live check. Whether -disableluarefresh is set on Limelight boxes,
which affects how easy the §2, §4 and §5 reload paths are to hit but not whether they exist.
Sources
Repo (this worktree, branch claude/wayfinder-3431-3434-32da3c at 06338d9a5)
gamemode/init.lua:19-21, 2397-2401, 2977-2980gamemode/db.lua:57-76, 139-196gamemode/sh_init.lua:11, 108-109, 217gamemode/core/sv_commands.lua:3109-3115, 3831gamemode/core/libraries/sv_logs.lua:121-135, 188-204gamemode/core/libraries/sv_player.lua:450-495, 925-945gamemode/core/libraries/sv_temar.lua:89, 414, 419-431gamemode/core/libraries/sh_tophints.lua:15-32gamemode/core/libraries/sh_plugin.lua:1046gamemode/core/migrations/0001_baseline_live_schema.lua:259-275, 616-618, 770-775, 835-840gamemode/core/migrations/0008_add_giveprops_loans.lua:1-20gamemode/core/plugins/refund_ents/sv_init.lua:18-34, 96, 126, 157, 162-230, 231-246gamemode/core/plugins/container_code/sv_dbcode.lua(whole file),sv_command_c.lua:98-105,sv_init.lua(whole file)gamemode/core/plugins/giveprops/sv_init.lua:62-141, 143-250gamemode/core/plugins/sv_auto_restart.lua:15-27, 43-46, 60-107gamemode/core/plugins/discord_relay_free/sv_init.lua:276-285gamemode/core/plugins/policedb/sv_init.lua:359-365,admin/sv_init.lua:215-217, 1085-1094gamemode/core/plugins/onlinelog/sv_init.lua:13-67gamemode/core/plugins/propsave/sv_init.lua:1-31, 53, 99, 155, 185, 197gamemode/core/plugins/carboosting/sv_init.lua:543-552, 4043-4058gamemode/core/plugins/daynight/sv_init.lua:30-95, 300-330gamemode/core/plugins/spawnpoints/sv_init.lua:108-125gamemode/core/plugins/beta/sv_init.lua:345,sv_item_db_update.lua:77entities/weapons/cityrp_lockpick/shared.lua:431, 616, 623addons/damagelog/lua/damagelogs/server/oldlogs.lua:11,server/recording.lua:164-165addons/pcasino/lua/perfectcasino/core/sv_core.lua:80, 117todo/server-session-system.md(whole file)AGENTS.mdfootguns #2, #7, #8, #11, #22
Prior research in this folder
docs/design/server-sessions/research-gm-initialize-boot-predicate.md(#3432)docs/design/server-sessions/research-fearlessrp-sessions-edge-cases.md(#3433)
Garry's Mod Wiki