CityRP Documentation Repository

Research: other boot-identity reinventions

Last updated

  • Ticket: limelight-development/cityrp2#3434
  • Parent map: #3431 — spec for a server-only cityrp.session library at gamemode/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, sinc and lua_refresh_file all re-execute a single file's scope without re-running gamemode/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 next PLUGIN:Load classifies 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.session compares a DB result value to a Lua number with == (sv_init.lua:179). If MySQL marshals that column as a string the comparison is false for 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-33 iterates 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-17 reads res[1]["json"] with no emptiness check, so /c crashed with 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_file on sv_logs.lua appends a Server Stop line dated one second ago followed by a Server Start line 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 on LLServer and nothing else, so a map reload — which sv_auto_restart.lua:93 performs on a timer and !map/!restart on demand — produces a Server Stop/Server Start pair 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-LLServer on disk (:24, :155). Not time-derived, not boot-scoped.
  • entities/weapons/cityrp_lockpick/shared.lua:431, 616, 623 — session is the per-attempt lockpicking minigame state.
  • referrals/sv_session.lua and referrals/cl_session.lua — filenames only; the contents are the referral UI. This is the one that matters, because the new library's file is gamemode/core/libraries/sv_session.lua and 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 that CityRP.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-552 that 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:

  1. 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) and giveprops — written by different authors across a decade of the codebase, none referencing another.

  2. 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.

  3. 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.

  4. 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_ents variant 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.

  5. Not inventing a heuristic is not a defence either. §7 shows what the alternative looks like when no shared source of truth exists: giveprops correctly declined to roll its own token, had nothing to scope its cleanup by, and shipped an unqualified DELETE that 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.

  6. server_stats is 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 at init.lua file 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.session can 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's connect = '<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_loans is 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 the DELETE needs a WHERE. Sized small; the migration is one column.

  • /c crashed grants 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 crashed errors when there is nothing to restore. container_code/sv_dbcode.lua:16-17 dereferences res[1]["json"] with no emptiness guard. Out of scope / cleanup

  • currentstats (migrations/0001:270-275) has no reader and no writer. Retire it alongside server_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-second file.Write (:133-135) is a per-second disk write whose only purpose is the Server Stop line 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) vs gamemode/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-2980
  • gamemode/db.lua:57-76, 139-196
  • gamemode/sh_init.lua:11, 108-109, 217
  • gamemode/core/sv_commands.lua:3109-3115, 3831
  • gamemode/core/libraries/sv_logs.lua:121-135, 188-204
  • gamemode/core/libraries/sv_player.lua:450-495, 925-945
  • gamemode/core/libraries/sv_temar.lua:89, 414, 419-431
  • gamemode/core/libraries/sh_tophints.lua:15-32
  • gamemode/core/libraries/sh_plugin.lua:1046
  • gamemode/core/migrations/0001_baseline_live_schema.lua:259-275, 616-618, 770-775, 835-840
  • gamemode/core/migrations/0008_add_giveprops_loans.lua:1-20
  • gamemode/core/plugins/refund_ents/sv_init.lua:18-34, 96, 126, 157, 162-230, 231-246
  • gamemode/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-250
  • gamemode/core/plugins/sv_auto_restart.lua:15-27, 43-46, 60-107
  • gamemode/core/plugins/discord_relay_free/sv_init.lua:276-285
  • gamemode/core/plugins/policedb/sv_init.lua:359-365, admin/sv_init.lua:215-217, 1085-1094
  • gamemode/core/plugins/onlinelog/sv_init.lua:13-67
  • gamemode/core/plugins/propsave/sv_init.lua:1-31, 53, 99, 155, 185, 197
  • gamemode/core/plugins/carboosting/sv_init.lua:543-552, 4043-4058
  • gamemode/core/plugins/daynight/sv_init.lua:30-95, 300-330
  • gamemode/core/plugins/spawnpoints/sv_init.lua:108-125
  • gamemode/core/plugins/beta/sv_init.lua:345, sv_item_db_update.lua:77
  • entities/weapons/cityrp_lockpick/shared.lua:431, 616, 623
  • addons/damagelog/lua/damagelogs/server/oldlogs.lua:11, server/recording.lua:164-165
  • addons/pcasino/lua/perfectcasino/core/sv_core.lua:80, 117
  • todo/server-session-system.md (whole file)
  • AGENTS.md footguns #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