Skip to content

Use @isaacs/ttlcache for honeypot config (#216)#274

Merged
vcarl merged 1 commit intomainfrom
auto/216-use-ttlcache-honeypot
Feb 6, 2026
Merged

Use @isaacs/ttlcache for honeypot config (#216)#274
vcarl merged 1 commit intomainfrom
auto/216-use-ttlcache-honeypot

Conversation

@vcarl
Copy link
Member

@vcarl vcarl commented Feb 6, 2026

Closes #216

Summary

Successfully replaced manual cache logic with TTLCache in honeypotTracker.ts. Added TTLCache import, replaced manual cache object with TTLCache instance (10 min TTL, max 1000 guilds), and simplified cache checking logic from ~18 lines to ~11 lines. All tests, linting, and typechecking passed.


Triage
{
  "action_type": "implement_issue",
  "target_id": 216,
  "target_title": "Use @isaacs/ttlcache for honeypot config",
  "reasoning": "This is an ideal candidate for autonomous work:\n- Small, well-defined refactoring task (replace manual cache with existing library)\n- Package @isaacs/ttlcache is already installed in package.json\n- Scope is limited to a single file (app/discord/honeypotTracker.ts)\n- Clear acceptance criteria: replace lines 18-59 manual cache implementation with TTLCache\n- Low risk: this is a code quality improvement with no external dependencies or runtime Discord testing needed\n- The manual cache pattern (configCache object with TTL checking) can be directly replaced with TTLCache's built-in functionality",
  "estimated_complexity": "small",
  "relevant_files": [
    "app/discord/honeypotTracker.ts"
  ],
  "branch_name": "auto/216-use-ttlcache-honeypot"
}
Context

Excellent! Now I have all the context I need. Let me create the structured context document:


Context Document: Issue #216 - Use @isaacs/ttlcache for honeypot config

Issue Summary

Replace the manual caching implementation in honeypotTracker.ts with the @isaacs/ttlcache library that's already in use elsewhere in the codebase (specifically in app/routes/__auth.tsx). This will standardize caching across the project and reduce custom code maintenance.

The current implementation uses a plain object with manual timestamp checking (configCache[guildId] with cachedAt timestamps), while the rest of the codebase uses the purpose-built @isaacs/ttlcache library.

Current Code State

app/discord/honeypotTracker.ts

Lines 15-21: Manual cache implementation

const CACHE_TTL_IN_MS = 1000 * 60 * 10; // reload cache entries every 10 minutes

export async function startHoneypotTracking(client: Client) {
  const configCache = {} as Record<
    string,
    { config: HoneypotConfig[]; cachedAt: number }
  >;

Lines 42-59: Manual cache checking and updating logic

const cacheEntry = configCache[msg.guildId];
if (!cacheEntry || cacheEntry.cachedAt + CACHE_TTL_IN_MS < Date.now()) {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache[msg.guildId] = { config, cachedAt: Date.now() };
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
} else {
  config = cacheEntry.config;
}

Current behavior:

  • Cache stores HoneypotConfig[] (array of configs) per guild ID
  • 10-minute TTL
  • Manual timestamp comparison for expiry
  • Logs when cache is populated

Database Schema

From app/db.d.ts:

export interface HoneypotConfig {
  channel_id: string;
  guild_id: string;
}

Patterns to Follow

TTLCache Usage Pattern (from app/routes/__auth.tsx)

Lines 4, 24-27: Import and instantiation

import TTLCache from "@isaacs/ttlcache";

// TTL cache for guild data - 5 minute TTL, max 100 users
const guildCache = new TTLCache<string, GuildData[]>({
  ttl: 5 * 60 * 1000, // 5 minutes
  max: 100, // max 100 users cached
});

Lines 39-44: Cache usage

// Check cache first
const cachedGuilds = guildCache.get(user.id);
if (cachedGuilds) {
  return {
    guilds: cachedGuilds,
    manageableGuilds: cachedGuilds.filter((g) => g.hasBot),
  };
}

Line 54: Cache population

guildCache.set(user.id, guilds);

Key observations:

  • TTLCache is instantiated with explicit ttl and max parameters
  • Simple .get() returns undefined if expired/missing
  • Simple .set() to populate
  • Comment documents the TTL and max settings
  • No manual timestamp checking required

Effect-TS Patterns

The honeypotTracker currently uses:

  • Legacy run() from AppRuntime (line 44)
  • Legacy log() from observability (lines 52-56, throughout)
  • No Effect integration currently

However, since this is not touching Effect code (it's just replacing the cache), we do not need to consult EFFECT.md or convert to Effect patterns. The issue is purely about cache replacement.

Constraints

  1. Maintain 10-minute TTL: Current implementation has CACHE_TTL_IN_MS = 1000 * 60 * 10
  2. Cache stores arrays: The cache value is HoneypotConfig[], not a single config
  3. Guild ID as key: Cache is keyed by msg.guildId (string)
  4. Preserve logging: Keep the debug log when cache is populated
  5. No breaking changes: The function signature and behavior must remain the same
  6. Package already installed: @isaacs/ttlcache is at version ^1.4.1 in package.json

Files to Modify

  1. app/discord/honeypotTracker.ts (primary change)
    • Add import for TTLCache
    • Replace manual cache object with TTLCache instance
    • Replace manual expiry checking with .get() call
    • Replace manual cache setting with .set() call
    • Remove CACHE_TTL_IN_MS constant (inline it into TTLCache config)

Related Test Files

No existing test files found for honeypotTracker. Test files exist for:

  • app/helpers/*.test.ts (various utility tests)
  • app/commands/escalate/strings.test.ts

Since this is a refactoring (behavior-preserving change) and there are no existing tests for honeypotTracker, no test updates are required. If tests existed, they should continue passing without modification.


Plan

Implementation Plan: Replace Manual Cache with @isaacs/ttlcache in honeypotTracker

Summary

This is a straightforward refactoring to replace ~40 lines of manual cache logic with a 5-line TTLCache implementation. Single file change, no behavioral modifications.


Step-by-Step Implementation

Step 1: Add TTLCache Import

File: app/discord/honeypotTracker.ts

Location: Top of file (line ~1-10, with other imports)

Change: Add import statement

// Before: (no TTLCache import)

// After:
import TTLCache from "@isaacs/ttlcache";

Why: Required to use the TTLCache class


Step 2: Replace Manual Cache Object with TTLCache Instance

File: app/discord/honeypotTracker.ts

Location: Lines 15-21 (inside startHoneypotTracking function)

Change: Replace the CACHE_TTL_IN_MS constant and configCache object with TTLCache instantiation

// Before:
const CACHE_TTL_IN_MS = 1000 * 60 * 10; // reload cache entries every 10 minutes

export async function startHoneypotTracking(client: Client) {
  const configCache = {} as Record<
    string,
    { config: HoneypotConfig[]; cachedAt: number }
  >;

// After:
export async function startHoneypotTracking(client: Client) {
  // TTL cache for honeypot configs - 10 minute TTL
  const configCache = new TTLCache<string, HoneypotConfig[]>({
    ttl: 10 * 60 * 1000, // 10 minutes
  });

Why:

  • Removes manual timestamp tracking
  • Follows the pattern from __auth.tsx
  • Stores HoneypotConfig[] directly (not wrapped in {config, cachedAt})
  • Explicit TTL configuration matches the existing 10-minute duration

Step 3: Replace Manual Cache Checking Logic

File: app/discord/honeypotTracker.ts

Location: Lines 42-59 (inside the message handler)

Change: Replace manual expiry checking and cache access with TTLCache .get() and .set()

// Before:
const cacheEntry = configCache[msg.guildId];
if (!cacheEntry || cacheEntry.cachedAt + CACHE_TTL_IN_MS < Date.now()) {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache[msg.guildId] = { config, cachedAt: Date.now() };
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
} else {
  config = cacheEntry.config;
}

// After:
const cachedConfig = configCache.get(msg.guildId);
if (cachedConfig) {
  config = cachedConfig;
} else {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache.set(msg.guildId, config);
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
}

Why:

  • .get() returns undefined if key missing or expired (no manual timestamp check needed)
  • .set() automatically handles TTL tracking
  • Preserves the debug logging behavior
  • Simplifies from ~18 lines to ~13 lines
  • Value stored directly as HoneypotConfig[] (no wrapping object)

Test Strategy

No new tests required because:

  1. No existing test files for honeypotTracker.ts
  2. This is a behavior-preserving refactoring (same inputs → same outputs)
  3. The cache logic is an internal implementation detail

Validation approach:

  • Run npm run validate to ensure no TypeScript errors
  • Run npm test to ensure no existing tests broke
  • Manual testing: Start the bot and verify honeypot tracking still works with proper caching

Validation

Expected npm run validate Output

  • ✅ TypeScript compilation passes (no type errors)
  • ✅ Linting passes (no ESLint errors)
  • ✅ All existing tests pass

Changes Summary

  • Files modified: 1 (app/discord/honeypotTracker.ts)
  • Lines added: ~7
  • Lines removed: ~12
  • Net change: -5 lines (simpler code)

Risks & Mitigation

Risk 1: TTLCache Behavior Differences

What: TTLCache might handle edge cases differently than manual implementation

Likelihood: Low

Mitigation:

  • TTLCache is battle-tested (already used in __auth.tsx)
  • The logic is functionally identical (check cache → miss → fetch → store)
  • Both expire after 10 minutes

Risk 2: Memory Usage Pattern Change

What: TTLCache has internal data structures that might use more/less memory

Likelihood: Low

Impact: Negligible (caching guild configs, not large objects)

Mitigation:

  • Could add max option to TTLCache if needed (like __auth.tsx has max: 100)
  • Current implementation has no limit either
  • Monitor in production if concerned

Risk 3: No Max Cache Size

What: The new implementation (like the old one) has no upper bound on cached guilds

Likelihood: Low (would need 1000s of active guilds to matter)

Mitigation:

  • Could add max parameter to TTLCache config if needed later
  • Example: max: 500 would limit to 500 guilds cached
  • Not adding now to minimize changes (follow existing behavior exactly)

Summary

Complexity: LOW ✅
Files changed: 1
Breaking changes: None
Dependencies: Already installed
Test impact: None (no existing tests)

This is a simple, safe refactoring that standardizes caching across the codebase. The change is localized to a single function and follows the exact pattern already used in __auth.tsx.

Critique

Issues Found

1. CRITICAL: Cache hit logic is inverted

The proposed Step 3 changes the cache hit behavior:

Current code:

if (!cacheEntry || cacheEntry.cachedAt + CACHE_TTL_IN_MS < Date.now()) {
  // Cache miss or expired: fetch from DB
} else {
  config = cacheEntry.config; // Cache hit: use cached value
}

Proposed code:

const cachedConfig = configCache.get(msg.guildId);
if (cachedConfig) {
  config = cachedConfig; // Cache hit
} else {
  // Cache miss: fetch from DB
}

This is correct — TTLCache returns undefined on miss/expiry, so checking if (cachedConfig) is the right pattern. However, the "After" code could be cleaner by eliminating the unnecessary config variable reassignment.

2. MINOR: Unnecessary variable assignment in cache hit path

The proposed code does:

if (cachedConfig) {
  config = cachedConfig; // unnecessary intermediate step
}

Could simplify to:

const config = configCache.get(msg.guildId);
if (!config) {
  // fetch and set
}

This matches the pattern more closely and eliminates redundant assignment.

3. MINOR: No consideration for max cache size

The plan acknowledges this but dismisses it. However, __auth.tsx sets max: 100 for a reason — unbounded caches can grow indefinitely. While the current implementation also has no limit, we're touching this code anyway, so adding a sensible max would be prudent.

Recommendation: Add max: 1000 (or similar) to match the defensiveness shown in __auth.tsx.


Suggested Revisions

Revision 1: Simplify Step 3 cache logic

Replace the proposed Step 3 code with:

// After:
let config = configCache.get(msg.guildId);
if (!config) {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache.set(msg.guildId, config);
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
}

Why:

  • Shorter (11 lines vs 13)
  • Eliminates redundant if (cachedConfig) { config = cachedConfig }
  • More direct: "get from cache, if missing fetch and populate"
  • Matches how you'd naturally write this with TTLCache

Revision 2: Add max cache size for safety

In Step 2, change:

// After:
export async function startHoneypotTracking(client: Client) {
  // TTL cache for honeypot configs - 10 minute TTL, max 1000 guilds
  const configCache = new TTLCache<string, HoneypotConfig[]>({
    ttl: 10 * 60 * 1000, // 10 minutes
    max: 1000, // max 1000 guilds cached
  });

Why:

  • Defensive programming (prevents unbounded growth)
  • Follows the pattern from __auth.tsx (which has max: 100)
  • 1000 guilds is a reasonable upper bound (bot would need to be in >1000 active servers)
  • LRU eviction means most-recently-used guilds stay cached

Revised Plan

Implementation Plan: Replace Manual Cache with @isaacs/ttlcache in honeypotTracker

Summary

This is a straightforward refactoring to replace ~40 lines of manual cache logic with a 5-line TTLCache implementation. Single file change, no behavioral modifications.


Step-by-Step Implementation

Step 1: Add TTLCache Import

File: app/discord/honeypotTracker.ts

Location: Top of file (line ~1-10, with other imports)

Change: Add import statement

import TTLCache from "@isaacs/ttlcache";

Why: Required to use the TTLCache class


Step 2: Replace Manual Cache Object with TTLCache Instance

File: app/discord/honeypotTracker.ts

Location: Lines 15-21 (inside startHoneypotTracking function)

Change: Replace the CACHE_TTL_IN_MS constant and configCache object with TTLCache instantiation

// Before:
const CACHE_TTL_IN_MS = 1000 * 60 * 10; // reload cache entries every 10 minutes

export async function startHoneypotTracking(client: Client) {
  const configCache = {} as Record<
    string,
    { config: HoneypotConfig[]; cachedAt: number }
  >;

// After:
export async function startHoneypotTracking(client: Client) {
  // TTL cache for honeypot configs - 10 minute TTL, max 1000 guilds
  const configCache = new TTLCache<string, HoneypotConfig[]>({
    ttl: 10 * 60 * 1000, // 10 minutes
    max: 1000, // max 1000 guilds cached
  });

Why:

  • Removes manual timestamp tracking
  • Follows the pattern from __auth.tsx
  • Stores HoneypotConfig[] directly (not wrapped in {config, cachedAt})
  • Explicit TTL configuration matches the existing 10-minute duration
  • Max size prevents unbounded cache growth (defensive programming)

Step 3: Replace Manual Cache Checking Logic

File: app/discord/honeypotTracker.ts

Location: Lines 42-59 (inside the message handler)

Change: Replace manual expiry checking and cache access with TTLCache .get() and .set()

// Before:
const cacheEntry = configCache[msg.guildId];
if (!cacheEntry || cacheEntry.cachedAt + CACHE_TTL_IN_MS < Date.now()) {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache[msg.guildId] = { config, cachedAt: Date.now() };
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
} else {
  config = cacheEntry.config;
}

// After:
let config = configCache.get(msg.guildId);
if (!config) {
  config = await run(
    db
      .selectFrom("honeypot_config")
      .selectAll()
      .where("guild_id", "=", msg.guildId),
  );

  configCache.set(msg.guildId, config);
  log(
    "debug",
    "HoneypotTracking",
    `Added config to in-memory cache for guildId ${msg.guildId}`,
  );
}

Why:

  • .get() returns undefined if key missing or expired (no manual timestamp check needed)
  • .set() automatically handles TTL tracking
  • Preserves the debug logging behavior
  • Simplifies from ~18 lines to ~11 lines
  • Value stored directly as HoneypotConfig[] (no wrapping object)
  • Cleaner logic flow: try cache → if miss, fetch and populate

Test Strategy

No new tests required because:

  1. No existing test files for honeypotTracker.ts
  2. This is a behavior-preserving refactoring (same inputs → same outputs)
  3. The cache logic is an internal implementation detail

Validation approach:

  • Run npm run validate to ensure no TypeScript errors
  • Run npm test to ensure no existing tests broke
  • Manual testing: Start the bot and verify honeypot tracking still works with proper caching

Validation

Expected npm run validate Output

  • ✅ TypeScript compilation passes (no type errors)
  • ✅ Linting passes (no ESLint errors)
  • ✅ All existing tests pass

Changes Summary

  • Files modified: 1 (app/discord/honeypotTracker.ts)
  • Lines added: ~6
  • Lines removed: ~12
  • Net change: -6 lines (simpler code)

Risks & Mitigation

Risk 1: TTLCache Behavior Differences

What: TTLCache might handle edge cases differently than manual implementation

Likelihood: Low

Mitigation:

  • TTLCache is battle-tested (already used in __auth.tsx)
  • The logic is functionally identical (check cache → miss → fetch → store)
  • Both expire after 10 minutes

Risk 2: Cache Eviction Due to Max Size

What: Adding max: 1000 means LRU eviction could remove configs for active guilds

Likelihood: Very Low (would need >1000 active guilds)

Impact: Minor (just results in cache miss and DB fetch)

Mitigation:

  • 1000 is a generous limit (bot would need massive scale to hit this)
  • LRU eviction means actively-used guilds stay cached
  • Previous implementation had no limit either, so this is strictly better
  • Can increase limit if needed in production

Risk 3: Empty Array Caching

What: If a guild has no honeypot configs, we cache an empty array []

Likelihood: High (many guilds won't have honeypot configs)

Impact: Neutral (reduces DB queries for guilds without configs — desirable!)

Mitigation: None needed — this is correct behavior


Summary

Complexity: LOW ✅
Files changed: 1
Breaking changes: None
Dependencies: Already installed
Test impact: None (no existing tests)

This is a simple, safe refactoring that standardizes caching across the codebase and adds defensive max-size limiting. The change is localized to a single function and follows the exact pattern already used in __auth.tsx.


Verdict

The plan is sound with minor improvements applied. The refactoring:

  • ✅ Correctly replaces manual cache logic with TTLCache
  • ✅ Maintains the exact same behavior (10-minute TTL, guild-keyed)
  • ✅ Follows existing patterns from __auth.tsx
  • ✅ Simplifies code (-6 lines)
  • ✅ Adds defensive max-size limiting
  • ✅ Requires no test changes (no existing tests)
  • ✅ Low risk, high confidence

The revised plan addresses the minor inefficiency in the original proposal and adds a sensible max cache size for defensive programming.

VERDICT: PROCEED

Implementation result
{
  "status": "success",
  "branch_name": "auto/216-use-ttlcache-honeypot",
  "files_changed": [
    "app/discord/honeypotTracker.ts"
  ],
  "validation_passed": true,
  "summary": "Successfully replaced manual cache logic with TTLCache in honeypotTracker.ts. Added TTLCache import, replaced manual cache object with TTLCache instance (10 min TTL, max 1000 guilds), and simplified cache checking logic from ~18 lines to ~11 lines. All tests, linting, and typechecking passed."
}

This PR was created autonomously by the improve.sh pipeline. Human review is required before merging.

…cker.ts. Added TTLCache import, replaced manual cache object with TTLCache instance (10 min TTL, max 1000 guilds), and simplified cache checking logic from ~18 lines to ~11 lines. All tests, linting, and typechecking passed. (#216)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Smoke Test Results

Metric Value
Image Size unknown
Startup Time 78s

Tested commit: 449e2f0

@vcarl vcarl merged commit bbab598 into main Feb 6, 2026
5 checks passed
@vcarl vcarl deleted the auto/216-use-ttlcache-honeypot branch February 6, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use @isaacs/ttlcache for honeypot config

1 participant