DOCS/CACHE_ARCHITECTURE.MD

Cache Architecture

GlassFrame Protocol keeps all of its state in memory via one shared primitive, ProtocolCache (src/core/Cache.js), rather than scattering plain Maps through every layer. Every cache instance below is a ProtocolCache, and every one of them can be put into debug mode so you can watch it operate at runtime - see "Turning on cache logging" below.

Why a shared cache primitive

  • One place to reason about TTL, eviction, and stats instead of five slightly-different Map patterns spread across layers.
  • Optional debug logging is a constructor flag, not something bolted on per-module after the fact.
  • getStats() returns hits/misses/sets/evictions/size for any cache at runtime, which is useful when you're tuning thresholds in config.js and want to know whether a value is actually being reused.
  • countByPrefix(prefix) counts non-expired entries whose key starts with a given prefix, without listing them. frame.getGuildMetrics(guildId) uses this to count a single server's flagged members and open cases out of the shared threat-score and case caches, which are keyed guildId:userId.

Caches in use

Cache Owner Key shape TTL Purpose
threat-scores ThreatEngine guildId:userId none (decay-based; pruned once decayed near zero) The running, decaying risk score every layer contributes signals to.
open-cases PunishmentEngine guildId:userId punishment.caseCooldownMs (default 30s) Prevents re-punishing or re-logging the same member while an incident is still fresh. Expiry length is the debounce window.
nuke-snapshots AntiNukeLayer guildId antiNuke.snapshotTtlMs (default 1h) A lightweight forensic snapshot of role/channel names and positions, captured the instant a threshold trips, kept only for reference.
recent-webhooks AntiNukeLayer webhook ID antiNuke.webhookAbuse.watchPeriodMs (default 5m) Maps a freshly created webhook back to whoever created it, so a message flood through that webhook (which carries no guild member of its own) can still be traced to an executor.
groq-verdicts GroqClient hash of the first 500 characters of the message aiModeration.cacheTtlMs (default 5m) Stops a burst of identical/near-identical messages from each costing a fresh Groq call.

Two more Maps exist outside ProtocolCache because they're pure sliding windows with no need for hit/miss stats: AntiRaidLayer.joinTimestamps / AntiRaidLayer.joinProfiles (cluster correlation) and AntiNukeLayer.actionLog / AntiNukeLayer.webhookMessageCounts. All four are swept lazily - old timestamps are filtered out on the next write rather than on a timer, since they're only ever read right after a write.

SmartLogger.recentLogs is a fixed-size array (last 30, newest first, not a ProtocolCache) recording every log event across every guild - it's what powers the owner-only !gf engine dashboard's Activity Log page. See docs/ENGINE.md.

Bounded-concurrency queues (frame.actionQueue, frame.aiQueue) and the internal metrics collector (frame.performance) are a separate concern from caching - see docs/PERFORMANCE.md.

Turning on cache logging

Pass debug: true when constructing GlassFrame:

const frame = new GlassFrame(client, {
  getLogChannel: (guild) => guild.channels.cache.find((c) => c.name === "security-logs"),
  debug: true
});

(Equivalently, set cache: { debug: true } in config.js - the constructor option takes priority if both are set.)

With debug mode on, every cache above prints a line like:

[GlassFrame:Cache:open-cases] SET 123456789012345678:987654321098765432
[GlassFrame:Cache:groq-verdicts] HIT h482910335
[GlassFrame:Cache:threat-scores] EXPIRE 123456789012345678:555555555555555555

SmartLogger has its own separate debug channel for the same flag ([GlassFrame:SmartLogger] merge ... / final edit ...), covering the event-aggregation behavior described in docs/PROTOCOL_LAYERS.md.

This is meant for local debugging while you tune config.js - leave it off in production, since it writes to console.log on every cache operation and will get noisy fast on an active server.

Memory growth

threat-scores is the only cache with no hard TTL, since a member's score needs to persist and decay rather than vanish at a fixed time. ThreatEngine prunes it every 10 minutes, removing any entry whose decayed score has fallen under 0.5 and hasn't been touched in at least four half-lives - by then it's noise, not signal. Every other cache either has a real TTL or is a self-trimming sliding window, so none of them grow unbounded over a long-running process.