Skip to content

Commit 3752a3a

Browse files
committed
fix: tighten hot-path session memory hygiene
1 parent 2964518 commit 3752a3a

10 files changed

Lines changed: 408 additions & 251 deletions

File tree

src/handlers/event.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ class MockSessionManager {
6868
groupId,
6969
userGroupId,
7070
injectedMemories: false,
71-
lastInjectionFactUuids: [],
7271
visibleFactUuids: [],
7372
messageCount: 0,
7473
pendingMessages: [],
@@ -263,7 +262,7 @@ class MockRedisCache {
263262
touchedGroupIds: string[] = [];
264263
metaByGroupId = new Map<
265264
string,
266-
{ lastQuery?: string; lastRefresh?: number; factUuids: string[] }
265+
{ lastQuery?: string; lastRefresh?: number }
267266
>();
268267

269268
async touch(groupId: string) {
@@ -485,7 +484,6 @@ describe("event handler", () => {
485484
const redisCache = new MockRedisCache();
486485
redisCache.metaByGroupId.set("group-1", {
487486
lastQuery: "resume refresh from redis",
488-
factUuids: [],
489487
});
490488
const graphitiAsync = new MockGraphitiAsync();
491489

@@ -644,7 +642,6 @@ describe("event handler", () => {
644642
const redisCache = new MockRedisCache();
645643
redisCache.metaByGroupId.set("group-1", {
646644
lastQuery: "refresh after compact restart",
647-
factUuids: [],
648645
});
649646
const graphitiAsync = new MockGraphitiAsync();
650647

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const graphiti: Plugin = (input: PluginInput) => {
9696
redisEvents,
9797
redisSnapshot,
9898
redisCache,
99+
graphitiClient,
99100
{
100101
idleRetentionMs: config.falkordb.sessionTtlSeconds * 1000,
101102
},

src/services/graphiti-async.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ export class GraphitiAsyncService {
2525
const entry: PersistentMemoryCacheEntry = {
2626
query: "primer",
2727
refreshedAt: Date.now(),
28-
facts: [],
2928
nodes: [],
30-
factUuids: [],
3129
nodeRefs: [],
3230
episodeSummaries: episodes.map((episode) =>
3331
`${episode.name}: ${episode.content}`.slice(0, 240)
@@ -63,9 +61,7 @@ export class GraphitiAsyncService {
6361
await this.cache.set(groupId, {
6462
query: normalized,
6563
refreshedAt: Date.now(),
66-
facts,
6764
nodes,
68-
factUuids: facts.map((fact) => fact.uuid),
6965
nodeRefs: nodes.map((node) => node.uuid),
7066
});
7167
})().catch((err) => logger.debug("Graphiti cache refresh failed", err))
@@ -83,7 +79,7 @@ export class GraphitiAsyncService {
8379
this.cache.get(groupId),
8480
this.cache.getMeta(groupId),
8581
]);
86-
const refreshQuery = current?.query || meta?.lastQuery;
82+
const refreshQuery = meta?.lastQuery || current?.query;
8783
if (refreshQuery) this.scheduleCacheRefresh(groupId, refreshQuery);
8884
}
8985
})().catch((err) => logger.debug("Graphiti drain failed", err)).finally(

src/services/hot-tier-slice.test.ts

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,24 @@ import { createCompactingHandler } from "../handlers/compacting.ts";
55
import { createMessagesHandler } from "../handlers/messages.ts";
66
import { SessionManager } from "../session.ts";
77
import { BatchDrainService } from "./batch-drain.ts";
8+
import { GraphitiAsyncService } from "./graphiti-async.ts";
89
import { RedisCacheService } from "./redis-cache.ts";
910
import { RedisClient } from "./redis-client.ts";
1011
import { RedisEventsService } from "./redis-events.ts";
1112
import { RedisSnapshotService } from "./redis-snapshot.ts";
1213

14+
const createGraphitiStub = () => ({
15+
getEpisodes() {
16+
return Promise.resolve([]);
17+
},
18+
searchMemoryFacts() {
19+
return Promise.resolve([]);
20+
},
21+
searchNodes() {
22+
return Promise.resolve([]);
23+
},
24+
});
25+
1326
describe("hot-tier vertical slice", () => {
1427
it("records local state, prepares injection, transforms messages, and serves compaction context without live MCP", async () => {
1528
const redis = new RedisClient({ endpoint: "redis://unused" });
@@ -24,9 +37,7 @@ describe("hot-tier vertical slice", () => {
2437
await redisCache.set("group-1", {
2538
query: "Continue the overhaul",
2639
refreshedAt: Date.now(),
27-
facts: [{ uuid: "fact-1", fact: "Graphiti remains async" }],
2840
nodes: [{ uuid: "node-1", name: "ContextOverhaul" }],
29-
factUuids: ["fact-1"],
3041
nodeRefs: ["node-1"],
3142
});
3243

@@ -37,6 +48,8 @@ describe("hot-tier vertical slice", () => {
3748
redisEvents,
3849
redisSnapshot,
3950
redisCache,
51+
createGraphitiStub() as never,
52+
{} as never,
4053
);
4154
manager.setParentId("session-1", null);
4255
manager.setState(
@@ -86,9 +99,9 @@ describe("hot-tier vertical slice", () => {
8699
transformOutput.messages[0].parts[0].text,
87100
"<session_memory",
88101
);
89-
assertStringIncludes(
90-
transformOutput.messages[0].parts[0].text,
91-
"<persistent_memory",
102+
assertEquals(
103+
transformOutput.messages[0].parts[0].text.includes("<persistent_memory"),
104+
false,
92105
);
93106

94107
const events = await redisEvents.getRecentSessionEvents(
@@ -126,6 +139,8 @@ describe("hot-tier vertical slice", () => {
126139
redisEvents,
127140
redisSnapshot,
128141
redisCache,
142+
createGraphitiStub() as never,
143+
{} as never,
129144
);
130145
manager.setParentId("session-1", null);
131146
manager.setState(
@@ -304,6 +319,53 @@ describe("hot-tier vertical slice", () => {
304319
});
305320
});
306321

322+
it("prefers remembered metadata query over stale cached query after drain success", async () => {
323+
const redis = new RedisClient({ endpoint: "redis://unused" });
324+
const redisCache = new RedisCacheService(redis, {
325+
ttlSeconds: 300,
326+
driftThreshold: 0.5,
327+
});
328+
329+
await redisCache.set("group-1", {
330+
query: "older cached query",
331+
refreshedAt: Date.now() - 60_000,
332+
nodes: [],
333+
nodeRefs: [],
334+
});
335+
await redisCache.rememberRefreshQuery("group-1", "newer remembered query");
336+
337+
const refreshCalls: Array<{ groupId: string; query: string }> = [];
338+
const graphitiAsync = new GraphitiAsyncService(
339+
{
340+
getEpisodes() {
341+
return Promise.resolve([]);
342+
},
343+
searchMemoryFacts() {
344+
return Promise.resolve([]);
345+
},
346+
searchNodes(input: { query: string; groupIds: string[] }) {
347+
refreshCalls.push({ groupId: input.groupIds[0], query: input.query });
348+
return Promise.resolve([]);
349+
},
350+
} as never,
351+
redisCache,
352+
{
353+
drainGroup() {
354+
return Promise.resolve({ status: "success" as const });
355+
},
356+
} as never,
357+
);
358+
359+
graphitiAsync.scheduleDrain("group-1");
360+
await new Promise((resolve) => setTimeout(resolve, 0));
361+
await new Promise((resolve) => setTimeout(resolve, 0));
362+
363+
assertEquals(refreshCalls, [{
364+
groupId: "group-1",
365+
query: "newer remembered query",
366+
}]);
367+
});
368+
307369
it("classifies drift deterministically at the configured threshold boundary", () => {
308370
const redis = new RedisClient({ endpoint: "redis://unused" });
309371
const redisCache = new RedisCacheService(redis, {
@@ -314,17 +376,13 @@ describe("hot-tier vertical slice", () => {
314376
const aligned = redisCache.classifyRefresh({
315377
query: "alpha beta",
316378
refreshedAt: Date.now(),
317-
facts: [],
318379
nodes: [],
319-
factUuids: [],
320380
nodeRefs: [],
321381
}, "alpha beta gamma delta");
322382
const drifted = redisCache.classifyRefresh({
323383
query: "alpha beta",
324384
refreshedAt: Date.now(),
325-
facts: [],
326385
nodes: [],
327-
factUuids: [],
328386
nodeRefs: [],
329387
}, "alpha delta epsilon");
330388

@@ -352,6 +410,8 @@ describe("hot-tier vertical slice", () => {
352410
redisEvents,
353411
redisSnapshot,
354412
redisCache,
413+
createGraphitiStub() as never,
414+
{} as never,
355415
);
356416
manager.setParentId("session-1", null);
357417
manager.setState(
@@ -362,9 +422,7 @@ describe("hot-tier vertical slice", () => {
362422
await redisCache.set("group-1", {
363423
query: "primer",
364424
refreshedAt: Date.now(),
365-
facts: [],
366425
nodes: [],
367-
factUuids: [],
368426
nodeRefs: [],
369427
episodeSummaries: ["Primer episode"],
370428
});
@@ -378,9 +436,7 @@ describe("hot-tier vertical slice", () => {
378436
await redisCache.set("group-1", {
379437
query: "older query",
380438
refreshedAt: Date.now() - 301_000,
381-
facts: [{ uuid: "fact-1", fact: "Stale fact" }],
382439
nodes: [],
383-
factUuids: ["fact-1"],
384440
nodeRefs: [],
385441
});
386442
const stalePrepared = await manager.prepareInjection(
@@ -406,14 +462,11 @@ describe("hot-tier vertical slice", () => {
406462
await redisCache.set("group-1", {
407463
query: "architecture token",
408464
refreshedAt: Date.now(),
409-
facts: [{
410-
uuid: "fact-1",
411-
fact:
412-
"Exact token ALPHA-RECALL-42 identifies the architecture decision",
465+
nodes: [{
466+
uuid: "node-1",
467+
name: "ALPHA-RECALL-42 architecture decision",
413468
}],
414-
nodes: [],
415-
factUuids: ["fact-1"],
416-
nodeRefs: [],
469+
nodeRefs: ["node-1"],
417470
});
418471

419472
const sameGroupManager = new SessionManager(
@@ -423,6 +476,8 @@ describe("hot-tier vertical slice", () => {
423476
redisEvents,
424477
redisSnapshot,
425478
redisCache,
479+
createGraphitiStub() as never,
480+
{} as never,
426481
);
427482
sameGroupManager.setParentId("session-b", null);
428483
sameGroupManager.setState(
@@ -437,6 +492,8 @@ describe("hot-tier vertical slice", () => {
437492
redisEvents,
438493
redisSnapshot,
439494
redisCache,
495+
createGraphitiStub() as never,
496+
{} as never,
440497
);
441498
otherGroupManager.setParentId("session-c", null);
442499
otherGroupManager.setState(
@@ -475,9 +532,7 @@ describe("hot-tier vertical slice", () => {
475532
await redisCache.set("group-1", {
476533
query: "old recall topic",
477534
refreshedAt: Date.now() - 301_000,
478-
facts: [{ uuid: "fact-1", fact: "Stale but still useful recall fact" }],
479535
nodes: [],
480-
factUuids: ["fact-1"],
481536
nodeRefs: [],
482537
});
483538

@@ -488,6 +543,8 @@ describe("hot-tier vertical slice", () => {
488543
redisEvents,
489544
redisSnapshot,
490545
redisCache,
546+
createGraphitiStub() as never,
547+
{} as never,
491548
);
492549
manager.setParentId("session-1", null);
493550
manager.setState(

0 commit comments

Comments
 (0)