diff --git a/packages/core/sdk/src/executor-cache.test.ts b/packages/core/sdk/src/executor-cache.test.ts
new file mode 100644
index 000000000..839e9c5cf
--- /dev/null
+++ b/packages/core/sdk/src/executor-cache.test.ts
@@ -0,0 +1,93 @@
+import { describe, expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { createExecutor, type Executor } from "./executor";
+import { Tenant } from "./ids";
+
+// Keep in sync with the unexported fallback cache defaults in executor.ts.
+const MEMORY_CACHE_CAPACITY = 2_048;
+const MEMORY_CACHE_TTL_MS = 10 * 60 * 1000;
+
+const makeExecutor = Effect.acquireRelease(
+ createExecutor({
+ tenant: Tenant.make("test-tenant"),
+ onElicitation: "accept-all",
+ }),
+ (executor) => executor.close().pipe(Effect.ignore),
+);
+
+const withFakeNow = (
+ initialNow: number,
+ run: (clock: { readonly advance: (ms: number) => void }) => Effect.Effect,
+): Effect.Effect =>
+ Effect.acquireUseRelease(
+ Effect.sync(() => {
+ const originalNow = Date.now;
+ let now = initialNow;
+ Date.now = () => now;
+ return {
+ advance: (ms: number) => {
+ now += ms;
+ },
+ restore: () => {
+ Date.now = originalNow;
+ },
+ };
+ }),
+ (clock) => run({ advance: clock.advance }),
+ (clock) => Effect.sync(clock.restore),
+ );
+
+describe("executor cache", () => {
+ it.effect("uses an in-memory fallback when no cache is configured", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor;
+
+ yield* executor.cache.set("a", "value");
+ expect(yield* executor.cache.get("a")).toBe("value");
+
+ yield* executor.cache.remove("a");
+ expect(yield* executor.cache.get("a")).toBeUndefined();
+ }),
+ ),
+ );
+
+ it.effect("expires fallback entries by TTL on get and size", () =>
+ withFakeNow(1_000, ({ advance }) =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const executor = yield* makeExecutor;
+
+ yield* executor.cache.set("a", "value");
+ expect(yield* executor.cache.size).toBe(1);
+
+ advance(MEMORY_CACHE_TTL_MS);
+
+ expect(yield* executor.cache.get("a")).toBeUndefined();
+ expect(yield* executor.cache.size).toBe(0);
+ }),
+ ),
+ ),
+ );
+
+ it.effect("refreshes fallback LRU position when an existing key is written", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const executor: Executor = yield* makeExecutor;
+
+ yield* executor.cache.set("a", "old");
+ for (let index = 0; index < MEMORY_CACHE_CAPACITY - 1; index += 1) {
+ yield* executor.cache.set(`key-${index}`, String(index));
+ }
+
+ yield* executor.cache.set("a", "new");
+ yield* executor.cache.set("overflow", "value");
+
+ expect(yield* executor.cache.get("a")).toBe("new");
+ expect(yield* executor.cache.get("key-0")).toBeUndefined();
+ expect(yield* executor.cache.get("key-1")).toBe("1");
+ }),
+ ),
+ );
+});
diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts
index 3ab3757dc..d60cb70e6 100644
--- a/packages/core/sdk/src/executor.ts
+++ b/packages/core/sdk/src/executor.ts
@@ -511,6 +511,7 @@ const makeMemoryCacheStore = (): KeyValueStore.KeyValueStore => {
Effect.sync(() => {
const now = Date.now();
evictExpired(now);
+ rows.delete(key);
rows.set(key, { value, expiresAt: now + MEMORY_CACHE_TTL_MS });
evictCapacity();
}),