Table of Contents
Database - History
/grim history browses past violations and play sessions for a player. This
page covers what is stored, how to query it, how to move it between backends,
and what current retention settings do. For backend selection, see the
Database overview.
What Gets Recorded
Every time a player joins, Grim opens a session and starts capturing metadata: the player UUID, the session start time, last activity time, server name, the player's client version and brand, the Grim version, and the server's version. Every violation the anti-cheat records during the session is appended as a violation entry carrying the check ID, stable key, violation level, timestamp, and verbose detail string.
The session closes on disconnect. If the server stops without closing an open
session, the next startup crash-sweeps those rows by stamping closed_at from
last_activity; history output can mark those sessions as crashed.
Sessions are numbered globally per player: Session 1 is the player's very first stored session, Session K is their most recent. The list view shows newest sessions first, but the session number itself is stable.
Commands
All read shapes need the grim.history permission. The aliases hist and
grimac history work everywhere grim history does.
List A Player's Sessions
/grim history <player>
/grim history <player> page <P>
Shows sessions newest-first, paginated by
database.history.entries-per-page. Each row gives the session number, Grim
version, configured server name, client version, duration (or current if the
player is online now), violation count, unique check count, and crashed marker
when applicable.
Tab-complete on <player> includes online players. With a non-empty partial
name, it also queries stored player identity records and returns recent
matching names. Page suggestions are constrained to valid pages for that
player.
Drill Into One Session
/grim history <player> session <N|latest>
/grim history <player> session <N|latest> page <P>
Shows the violation detail for Session N. latest, last, and l are
aliases for the most recent stored session.
Violations are paginated within the session. The default view groups
violations into time buckets using database.history.group-interval-ms.
Use --detailed or -d to see one row per violation, and --verbose or -v
to inline the verbose string instead of leaving it only on hover.
/grim history <player> session 42 --detailed --verbose
/grim history <player> session latest -d -v
Filters
All list and detail shapes accept regex filters:
/grim history <player> --name <regex>
/grim history <player> --match <regex>
/grim history <player> --grep <regex>
/grim history <player> session latest --name <regex> --match <regex>
--name <regex>matches the check display name.--match <regex>matches the verbose string.--grep <regex>matches either the check display name or the verbose string.
Filters are case-insensitive. Combining filters narrows results with AND behavior. In the session-list view, filtering keeps only sessions on the visible page that have at least one matching violation.
Migration / Copy Commands
If you switch backends and want to bring existing data along, separate commands handle that. They are permission-gated independently:
/grim history migrate [--delete] # grim.history.migrate
/grim history copy <src-backend-id> <dst-backend-id> [--delete] # grim.history.copy
migrate imports legacy v0 grim_history_* history into the current v1
layout. It detects the old source from legacy history.database.* config or
the old violations.sqlite location. The current public migration command
needs a SQLite backend in routing, because the v0 migrator writes through the
SQLite import path. --delete drops the old v0 tables after a successful
migration.
copy copies current v1 history from one configured backend id to another.
Source and destination are resolved from the active routing. --delete wipes
the source backend after the copy completes.
Caution
Running
copytwice into the same destination can duplicate violation rows. Sessions deduplicate by session id, but violation rows receive destination-local ids.
Where The Data Lives
History uses two datastore categories:
sessionviolation
The default public routing sends both to sqlite:
database:
routing:
violation: sqlite
session: sqlite
For shared network history, route both categories to the same networked backend:
database:
routing:
violation: mysql
session: mysql
player-identity: mysql
setting: sqlite
blob: none
Player-name tab completion uses the player-identity category. If player
identity is routed to none, history lookup by exact platform-known names can
still work, but stored offline-name completion is unavailable.
The six logical storage names on relational backends are:
grim_metagrim_checksgrim_playersgrim_sessionsgrim_violationsgrim_settings
Names are configurable under each backend's tables: block in
databases/<backend-id>.yml. MongoDB uses these as collection names. Redis
uses them as key namespace segments.
Internals - Stable Keys Vs. Local Check IDs
Each Grim check has two identities:
- Stable key - a globally-stable string like
grim.simulation,grim.knockback,grim.reach, orgrim.timer.tick. It is declared on the check class via@CheckData(stableKey = ...). Same value across every Grim install, version, and backend. - Local check ID - an integer assigned by the backend's check catalog the first time the check is seen in that database. Violation rows reference this local id.
Caution
Do not raw-copy violation rows between independent databases unless you also preserve and remap the check catalog. In DB-A,
check_id=42might meangrim.knockback; in DB-B,42might meangrim.reach. A raw copy can silently mis-attribute every violation in the moved data.Use
/grim history copy <src> <dst>instead. It resolves stable keys and writes the destination's local ids correctly.
The same applies to session IDs and player UUIDs in the opposite direction:
those are UUIDs and globally unique, so they are safe to use as identifiers.
Only check_id is database-local.
If you are writing tooling that reads Grim's tables directly, join through the
check catalog's stable_key rather than caching local check_id values.
SQLite File Portability Across Server Versions
The SQLite schema is identical across supported Minecraft server versions. A
history.v1.db file written on a legacy Bukkit-family server can be moved to a
newer Paper server and Grim will read and continue writing to it without a
schema rewrite.
This portability is about schema compatibility, not check-ID portability.
Moving a single SQLite file preserves local check IDs because the file's
grim_checks table moves with it. Copying violations between two SQLite files
needs /grim history copy for the same reason MySQL needs it.
Internally Grim uses two writer dialects depending on the live SQLite engine:
a single-statement INSERT ... ON CONFLICT DO UPDATE upsert on engines 3.24+
(modern), and a two-statement INSERT OR IGNORE + UPDATE pattern wrapped in
a transaction on older engines (legacy). Both write the same column set with
the same values, so resulting row state is equivalent; only the SQL differs.
The dialect is picked once at backend init from SELECT sqlite_version() and
logged.
Retention
database.yml contains retention rules for sessions, violations, player
identity, settings, and blobs:
database:
retention:
session:
enabled: true
max-age-days: 90
violation:
enabled: true
max-age-days: 365
player-identity:
enabled: false
setting:
enabled: false
blob:
enabled: true
max-age-days: 30
Current public code parses these rules and has a storage-layer retention sweeper, but Grim does not currently schedule that sweeper. Treat the settings as configuration for the retention path, not as guaranteed automatic pruning until scheduling is wired.
The sweeper implementation only applies session and violation retention.
player-identity and setting rules are parsed but skipped.
Performance Notes
- SQLite writes go through the WAL journal and per-handler transactions, so
concurrent reads from
/grim historycan run alongside writes. WAL is the default; do not change it unless you know why. - MySQL / Postgres use batched statements. MySQL adds
rewriteBatchedStatements=true; Postgres users can addreWriteBatchedInserts=truethroughextra-jdbc-params. - MongoDB writes are bulk-acknowledged. Tune write concern and replica-set behavior through the connection string.
- Redis is low-latency for writes but is not ideal for the read patterns
/grim historydoes, such as page-by-time and list-by-player. Use a relational or document backend if you will read history often.
Failure Handling
If database.enabled is false, or a configured backend cannot initialize at
startup, datastore services are disabled for that run. History reads/writes
then do not persist, /grim history reports that history is disabled or failed
to load, and live anti-cheat checks keep running.
Current public behavior does not silently fall back to memory. Route a
category to memory explicitly if you want ephemeral storage.
Customising The Output
All /grim history text comes from messages.yml under the
grim-history-* keys. Recolour, reword, or change formatting without touching
code. The grim-history-detail-entry template accepts an optional
%description% variable; include it to inline the check's short description in
detailed-mode rows. The default omits it to keep rows narrow, and the
description always appears on hover where hover is supported.
Available pages
More information in the readme.