openclaw - ✅(Solved) Fix Bug: DiscordEntityCache REST entity Map grows unbounded across bot lifetime [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77975Fetched 2026-05-06 06:18:31
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
2
Timeline (top)
cross-referenced ×2commented ×1

Fix Action

Fix / Workaround

Repro is straightforward: drive the unpatched fetchCached path with N unique keys → Map grows to N regardless of TTL.

PR fix notes

PR #77952: fix(discord): bound REST entity cache to prevent unbounded Map growth

Description (problem / solution / changelog)

Closes #77975

Summary

DiscordEntityCache (extensions/discord/src/internal/entity-cache.ts) cached user:<id>, channel:<id>, guild:<id>, and member:<gid>:<uid> REST fetches in a Map with a 30s TTL, but only checked the TTL on an exact-same-key re-fetch inside fetchCached. Different keys never triggered any sweep, so on a long-lived gateway the cache grew unbounded across the bot's lifetime — every distinct user/channel/guild touched stayed retained past its TTL until process restart. A slow leak proportional to entity cardinality.

This change adds:

  • A throttled write-time sweep (maybeSweepExpired, default once per 30s) that walks the Map and drops expired entries.
  • A hard maxEntries cap (default 5,000) enforced via FIFO drop using Map insertion order.
  • Optional constructor params maxEntries / sweepIntervalMs for callers that want to tune.
  • A read-only size getter for diagnostics.

The existing ttlMs? constructor option, public fetch surface, and invalidateForGatewayEvent semantics are unchanged.

Verification

  • New colocated entity-cache.test.ts: 4 cases — cap enforcement, post-TTL sweep, no-sweep before interval, ttlMs=0 disables caching. All pass.
  • oxfmt --check clean on the touched files.
  • Live demo run included below in Real behavior proof.

Real behavior proof

  • Behavior or issue addressed: DiscordEntityCache.entries is a Map with read-time TTL check only and no write-time eviction or hard size cap, so on a long-lived gateway the cache grows unbounded as different user:/channel:/guild:/member: keys are touched. The 30s TTL is never enforced for keys that aren't re-fetched.
  • Real environment tested: local node v22.22.2 runtime (no Discord network); the patched DiscordEntityCache was driven by a fake RequestClient returning { id } for every route, so the cache exercises real eviction code paths under high cardinality without going through any test framework.
  • Exact steps or command run after this patch: wrote /tmp/discord-cache-demo.ts that imports the patched cache, runs 100,000 unique-key fetches against the cap, then runs a sweep-window scenario with ttlMs=10/sweepIntervalMs=0. Executed via pnpm exec tsx /tmp/discord-cache-demo.ts — direct node runtime output, no mocking framework involved.
  • Evidence after fix: live console output captured directly from the node runtime:
=== capped run (default maxEntries=5000, ttlMs=60s) ===
inserted 0 size: 1
inserted 10000 size: 5000
inserted 20000 size: 5000
inserted 30000 size: 5000
inserted 40000 size: 5000
inserted 50000 size: 5000
inserted 60000 size: 5000
inserted 70000 size: 5000
inserted 80000 size: 5000
inserted 90000 size: 5000
final size after 100k unique fetches: 5000

=== sweep run (maxEntries=10_000, ttlMs=10ms, sweep=0) ===
size after 5k inserts (TTL 10ms): 456
size after a single insert past TTL window: 1
  • Observed result after fix: with maxEntries=5000, the cache stays pinned at 5,000 entries across 100,000 unique fetches (oldest-first eviction holds the cap). With ttlMs=10ms and sweepIntervalMs=0, one insert past the TTL window sweeps the prior 456 stale entries down to 1, confirming the write-time expiry sweep works.
  • What was not tested: behavior against a real Discord gateway with an active bot session — the fix is purely a Map/cache change with no network surface, and the live runtime demo exercises the same eviction code paths under higher cardinality than a real bot would see.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/discord/src/internal/entity-cache.test.ts (added, +77/-0)
  • extensions/discord/src/internal/entity-cache.ts (modified, +42/-1)
RAW_BUFFERClick to expand / collapse

Problem

extensions/discord/src/internal/entity-cache.ts caches user:, channel:, guild:, and member: REST fetches in a Map with a 30-second TTL. The TTL is only checked on a same-key re-fetch inside fetchCached. Different keys never trigger any sweep, and there is no max-size cap.

On a long-lived gateway, every distinct entity the bot has ever touched stays retained past its TTL until process restart. Slow, predictable leak proportional to entity cardinality.

Repro is straightforward: drive the unpatched fetchCached path with N unique keys → Map grows to N regardless of TTL.

Tracking PR

Fix in #77952.

extent analysis

TL;DR

Implement a mechanism to periodically sweep the cache and remove expired entries to prevent memory leaks.

Guidance

  • Introduce a timer-based sweep mechanism to remove expired entries from the cache, ensuring the TTL is respected for all entries, not just those re-fetched with the same key.
  • Consider adding a max-size cap to the cache to prevent unbounded growth and mitigate the impact of the leak.
  • Review the fetchCached function to ensure it correctly handles cache expiration and removal.
  • Test the cache sweep mechanism with a variety of scenarios, including multiple unique keys and expired entries.

Example

// Example of a simple timer-based sweep mechanism
setInterval(() => {
  const now = Date.now();
  cache.forEach((value, key) => {
    if (now - value.timestamp > TTL) {
      cache.delete(key);
    }
  });
}, SWEEP_INTERVAL);

Notes

The provided example is a basic illustration and may need to be adapted to the specific requirements of the entity-cache.ts implementation.

Recommendation

Apply workaround: Implement a cache sweep mechanism to prevent memory leaks, as a fix is already in progress (tracked in #77952).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix Bug: DiscordEntityCache REST entity Map grows unbounded across bot lifetime [1 pull requests, 1 comments, 2 participants]