Skip to content

Conversation

@0xbbjoker
Copy link
Collaborator

@0xbbjoker 0xbbjoker commented Jan 5, 2026

Summary

Fixes 6 issues identified by AI code review in the CachedDatabaseAdapter:


Note

Introduces a caching wrapper and runtime optimization to reduce DB and model calls.

  • New CachedDatabaseAdapter: L1 in-memory LRU with optional L2 external cache; read-through on misses, targeted invalidation on mutations; supports agents, entities, rooms, worlds, participants, components, relationships, tasks; passthrough for high-volume memory ops; exposed via index.ts.
  • External cache support: Pluggable adapter interface with key prefixing; factory createCachedAdapter.
  • Runtime optimization: AgentRuntime now caches embedding dimension; adds getEmbeddingDimension()/setEmbeddingDimension() (validated against VECTOR_DIMS); init uses pre-set dimension or falls back to probing when embedding model exists.
  • Tests: Extensive integration coverage (TTL expiry, invalidation paths, batch ops, external cache) in cached-adapter.test.ts.

Written by Cursor Bugbot for commit 0ca8e4e. This will update automatically on new commits. Configure here.

Greptile Summary

This PR adds a CachedDatabaseAdapter wrapper that provides LRU caching with optional external cache support (Redis/Upstash) for serverless environments, plus embedding dimension caching in the runtime to avoid redundant model calls.

Key Changes:

  • New CachedDatabaseAdapter class implementing two-tier caching strategy (in-memory L1 + optional external L2)
  • Runtime embedding dimension is now cached and can be pre-configured via setEmbeddingDimension()
  • Automatic cache invalidation on mutations (updates, deletes, creates)
  • Comprehensive test coverage (1,530 lines) covering all caching scenarios, TTL expiration, and external cache integration
  • Smart invalidation strategy: individual entity caches are updated on mutation, while aggregate caches (like entitiesForRoom) are cleared

Cache Strategy:

  • Read-through caching: Check L1 → L2 → Database, populating caches on miss
  • Write-through invalidation: Mutations invalidate affected cache entries
  • Configurable TTL per cache type with LRU eviction
  • Memory operations (high volume) are NOT cached to avoid excessive memory usage

Note: This is marked as a DRAFT PR and should NOT be merged yet.

Confidence Score: 3/5

  • This PR introduces significant caching infrastructure but has syntax issues and potential logic bugs that need resolution before merging
  • Score reflects excellent test coverage and well-designed caching architecture, but is reduced due to: (1) syntax errors in optional method declarations that will cause TypeScript compilation issues, (2) unsafe type casting in createAgent that could cache incomplete data, and (3) this being a DRAFT PR explicitly marked "DO NOT MERGE". The core caching logic is sound and thoroughly tested, but the syntax issues must be fixed for production readiness.
  • packages/plugin-sql/src/cached-adapter.ts requires syntax fixes for optional method declarations (lines 870-909) and logic review for type casting on line 297

Important Files Changed

Filename Overview
packages/core/src/runtime.ts Added embedding dimension caching with getter/setter methods to optimize serverless environments by avoiding redundant model calls
packages/plugin-sql/src/cached-adapter.ts New LRU cache wrapper for database adapter with two-tier caching (in-memory + optional external cache like Redis/Upstash) for serverless optimization
packages/plugin-sql/src/tests/integration/cached-adapter.test.ts Comprehensive integration tests covering all caching scenarios, invalidation logic, TTL expiration, and external cache adapter support
packages/plugin-sql/src/index.ts Exported new cached adapter types and factory function for public API

Sequence Diagram

sequenceDiagram
    participant Runtime as AgentRuntime
    participant CachedAdapter as CachedDatabaseAdapter
    participant L1Cache as In-Memory LRU Cache
    participant L2Cache as External Cache (Redis/Upstash)
    participant BaseAdapter as Base Database Adapter
    participant DB as PostgreSQL/PGLite

    Note over Runtime: Initialization
    Runtime->>Runtime: Check embeddingDimension cache
    alt Pre-configured dimension
        Runtime->>CachedAdapter: ensureEmbeddingDimension(dimension)
        CachedAdapter->>BaseAdapter: ensureEmbeddingDimension(dimension)
        BaseAdapter->>DB: Configure vector dimension
    else Dimension not cached
        Runtime->>Runtime: getModel(TEXT_EMBEDDING)
        Runtime->>Runtime: Generate test embedding
        Runtime->>Runtime: Cache embedding.length
        Runtime->>CachedAdapter: ensureEmbeddingDimension(embedding.length)
        CachedAdapter->>BaseAdapter: ensureEmbeddingDimension(embedding.length)
        BaseAdapter->>DB: Configure vector dimension
    end

    Note over Runtime,DB: Read Operations (Cache Hit)
    Runtime->>CachedAdapter: getAgent(agentId)
    CachedAdapter->>L1Cache: get(agentId)
    L1Cache-->>CachedAdapter: Agent data
    CachedAdapter-->>Runtime: Agent data

    Note over Runtime,DB: Read Operations (L1 Miss, L2 Hit)
    Runtime->>CachedAdapter: getRoom(roomId)
    CachedAdapter->>L1Cache: get(roomId)
    L1Cache-->>CachedAdapter: undefined
    CachedAdapter->>L2Cache: get(cacheKey)
    L2Cache-->>CachedAdapter: Room data
    CachedAdapter->>L1Cache: set(roomId, room)
    CachedAdapter-->>Runtime: Room data

    Note over Runtime,DB: Read Operations (Cache Miss)
    Runtime->>CachedAdapter: getEntity(entityId)
    CachedAdapter->>L1Cache: get(entityId)
    L1Cache-->>CachedAdapter: undefined
    CachedAdapter->>L2Cache: get(cacheKey)
    L2Cache-->>CachedAdapter: undefined
    CachedAdapter->>BaseAdapter: getEntity(entityId)
    BaseAdapter->>DB: SELECT entity
    DB-->>BaseAdapter: Entity data
    BaseAdapter-->>CachedAdapter: Entity data
    CachedAdapter->>L1Cache: set(entityId, entity)
    CachedAdapter->>L2Cache: set(cacheKey, entity, ttl)
    CachedAdapter-->>Runtime: Entity data

    Note over Runtime,DB: Write Operations (Cache Invalidation)
    Runtime->>CachedAdapter: updateAgent(agentId, updates)
    CachedAdapter->>BaseAdapter: updateAgent(agentId, updates)
    BaseAdapter->>DB: UPDATE agent
    DB-->>BaseAdapter: Success
    BaseAdapter-->>CachedAdapter: Success
    CachedAdapter->>L1Cache: delete(agentId)
    CachedAdapter->>L2Cache: delete(cacheKey)
    CachedAdapter-->>Runtime: Success

    Note over Runtime,DB: Batch Operations
    Runtime->>CachedAdapter: getRoomsByIds([id1, id2, id3])
    CachedAdapter->>L1Cache: get(id1)
    L1Cache-->>CachedAdapter: Room1
    CachedAdapter->>L1Cache: get(id2)
    L1Cache-->>CachedAdapter: undefined
    CachedAdapter->>L1Cache: get(id3)
    L1Cache-->>CachedAdapter: undefined
    CachedAdapter->>BaseAdapter: getRoomsByIds([id2, id3])
    BaseAdapter->>DB: SELECT rooms WHERE id IN (...)
    DB-->>BaseAdapter: [Room2, Room3]
    BaseAdapter-->>CachedAdapter: [Room2, Room3]
    CachedAdapter->>L1Cache: set(id2, Room2)
    CachedAdapter->>L1Cache: set(id3, Room3)
    CachedAdapter-->>Runtime: [Room1, Room2, Room3]
Loading

…erless optimizations

- Add CachedDatabaseAdapter wrapper in plugin-sql that provides LRU caching
  for entities, rooms, worlds, agents, participants, components, relationships,
  and tasks with automatic cache invalidation on mutations

- Support optional ExternalCacheAdapter interface for Redis/Upstash integration
  in serverless environments (two-tier L1/L2 caching)

- Add embedding dimension caching to AgentRuntime with setEmbeddingDimension()
  method to skip model calls on serverless cold starts

- Add getEmbeddingDimension() and setEmbeddingDimension() to runtime with
  validation against VECTOR_DIMS constants

- Export CachedDatabaseAdapter, createCachedAdapter, CachedAdapterConfig,
  and ExternalCacheAdapter from plugin-sql

- Add comprehensive integration tests with 100% code coverage (73 tests)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 5, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cached-database-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Contributor

claude bot commented Jan 5, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

if (room?.worldId) {
await this.deleteFromCache(this.roomsByWorldCache, 'roomsByWorld', room.worldId);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteRoom fails to invalidate cache when room not cached

In deleteRoom, the code retrieves the room from cache only (via getFromCache) to determine its worldId for invalidating roomsByWorldCache. If the room was never cached (never read before deletion), the worldId won't be found and the roomsByWorldCache for that world won't be invalidated. This causes stale cache entries where getRoomsByWorld may return deleted rooms until TTL expiration. The method should fetch from the database (not just cache) to get the worldId before deletion.

Fix in Cursor Fix in Web

this.taskCache.clear();
this.roomsByWorldCache.clear();
this.entitiesForRoomCache.clear();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearAllCaches doesn't clear the external L2 cache

The clearAllCaches() method only clears the L1 in-memory caches but does not call this.externalCache.clear() when an external cache (Redis, Upstash) is configured. The ExternalCacheAdapter interface defines a clear(prefix?) method specifically for this purpose. After calling clearAllCaches(), subsequent reads will still retrieve stale data from the L2 external cache and repopulate L1, making the cache-clearing operation ineffective when external caching is enabled.

Fix in Cursor Fix in Web

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 870 to 882
// ==================== Optional Methods ====================

async runPluginMigrations?(
plugins: Array<{
name: string;
schema?: Record<string, string | number | boolean | null | Record<string, unknown>>;
}>,
options?: { verbose?: boolean; force?: boolean; dryRun?: boolean }
): Promise<void> {
if (this.baseAdapter.runPluginMigrations) {
return this.baseAdapter.runPluginMigrations(plugins, options);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Optional method signature uses deprecated syntax

The async runPluginMigrations?() syntax is non-standard. TypeScript optional methods should be declared as runPluginMigrations?: (params) => Promise<void> or implement the interface method properly.

Suggested change
// ==================== Optional Methods ====================
async runPluginMigrations?(
plugins: Array<{
name: string;
schema?: Record<string, string | number | boolean | null | Record<string, unknown>>;
}>,
options?: { verbose?: boolean; force?: boolean; dryRun?: boolean }
): Promise<void> {
if (this.baseAdapter.runPluginMigrations) {
return this.baseAdapter.runPluginMigrations(plugins, options);
}
}
async runPluginMigrations(
plugins: Array<{
name: string;
schema?: Record<string, string | number | boolean | null | Record<string, unknown>>;
}>,
options?: { verbose?: boolean; force?: boolean; dryRun?: boolean }
): Promise<void> {
if (this.baseAdapter.runPluginMigrations) {
return this.baseAdapter.runPluginMigrations(plugins, options);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/plugin-sql/src/cached-adapter.ts
Line: 870:882

Comment:
**syntax:** Optional method signature uses deprecated syntax

The `async runPluginMigrations?()` syntax is non-standard. TypeScript optional methods should be declared as `runPluginMigrations?: (params) => Promise<void>` or implement the interface method properly.

```suggestion
  async runPluginMigrations(
    plugins: Array<{
      name: string;
      schema?: Record<string, string | number | boolean | null | Record<string, unknown>>;
    }>,
    options?: { verbose?: boolean; force?: boolean; dryRun?: boolean }
  ): Promise<void> {
    if (this.baseAdapter.runPluginMigrations) {
      return this.baseAdapter.runPluginMigrations(plugins, options);
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

@claude
Copy link
Contributor

claude bot commented Jan 5, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

await this.setInCache(this.agentCache, 'agent', agent.id, agent as Agent);
}
return result;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createAgent caches partial data as complete Agent type

The createAgent method receives a Partial<Agent> and caches it cast to Agent. The Agent interface requires createdAt and updatedAt fields, which are typically set by the database during creation, not provided by the caller. When getAgent is subsequently called before any update/delete, it returns this incomplete cached data missing required timestamp fields. This violates the type contract and can cause runtime errors when code accesses the expected fields.

Fix in Cursor Fix in Web

}

return results.length > 0 ? results : null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch fetch methods return items in wrong order

Both getEntitiesByIds and getRoomsByIds return results in incorrect order when some items are cached and others need database fetching. Cached items are pushed first, then database-fetched items are appended at the end. For example, requesting [id1, id2, id3] where only id2 is not cached returns [entity1, entity3, entity2] instead of preserving the input order. Code relying on result order matching request order will behave incorrectly.

Additional Locations (1)

Fix in Cursor Fix in Web

@claude
Copy link
Contributor

claude bot commented Jan 5, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@0xbbjoker 0xbbjoker marked this pull request as draft January 5, 2026 19:24
if (this.externalCache) {
await this.externalCache.delete(this.cacheKey(type, id));
}
}

This comment was marked as outdated.

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.

4 participants