Skip to content

Commit 89898b8

Browse files
committed
chore: optimization run
1 parent 1f34741 commit 89898b8

11 files changed

Lines changed: 285 additions & 105 deletions

src/handlers/chat.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,28 @@ describe("chat handler", () => {
404404
assertEquals(graphitiAsync.refreshCalls, []);
405405
assertEquals(graphitiAsync.drainCalls, []);
406406
});
407+
408+
it("skips session resolution and hot-tier work when no text prompt is present", async () => {
409+
const sessionManager = new MockSessionManager();
410+
const redisEvents = new MockRedisEvents();
411+
const graphitiAsync = new MockGraphitiAsync();
412+
413+
const handler = createChatHandler({
414+
sessionManager: sessionManager as never,
415+
redisEvents: redisEvents as never,
416+
graphitiAsync: graphitiAsync as never,
417+
drainTriggerSize: 99,
418+
});
419+
420+
await handler(
421+
{ sessionID: "session-1" },
422+
{ parts: [{ type: "file", path: "src/index.ts" }] } as never,
423+
);
424+
425+
assertEquals(sessionManager.activeCalls, []);
426+
assertEquals(sessionManager.prepareInjectionCalls, []);
427+
assertEquals(redisEvents.calls, []);
428+
assertEquals(graphitiAsync.refreshCalls, []);
429+
assertEquals(graphitiAsync.drainCalls, []);
430+
});
407431
});

src/handlers/chat.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export function createChatHandler(deps: ChatHandlerDeps): ChatMessageHook {
2424
return async ({ sessionID }: ChatMessageInput, output: ChatMessageOutput) => {
2525
try {
2626
sessionManager.markSessionActive(sessionID);
27+
28+
const messageText = extractTextFromParts(output.parts);
29+
if (!messageText) return;
30+
const sanitizedMessageText = sanitizeMemoryInput(messageText);
31+
if (!sanitizedMessageText) return;
32+
2733
const { state, resolved, canonicalSessionId } = await sessionManager
2834
.resolveSessionState(
2935
sessionID,
@@ -32,11 +38,6 @@ export function createChatHandler(deps: ChatHandlerDeps): ChatMessageHook {
3238
if (!canonicalSessionId) return;
3339
sessionManager.markResolvedSessionActive(sessionID, canonicalSessionId);
3440

35-
const messageText = extractTextFromParts(output.parts);
36-
if (!messageText) return;
37-
const sanitizedMessageText = sanitizeMemoryInput(messageText);
38-
if (!sanitizedMessageText) return;
39-
4041
state.messageCount += 1;
4142
state.latestUserRequest = sanitizedMessageText;
4243
state.latestRefreshQuery = sanitizedMessageText;

src/handlers/messages.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,4 +759,26 @@ describe("messages handler", () => {
759759

760760
assertEquals(output.messages[0].parts[0].text, "startup prompt");
761761
});
762+
763+
it("skips transform work when the latest user entry has no text part", async () => {
764+
const sessionManager = new MockSessionManager();
765+
sessionManager.prepareInjectionImpl = () => {
766+
throw new Error("prepareInjection should not run");
767+
};
768+
const handler = createMessagesHandler({
769+
sessionManager: sessionManager as never,
770+
});
771+
772+
const output = {
773+
messages: [{
774+
info: { role: "user", sessionID: "session-1" },
775+
parts: [{ type: "file", path: "src/index.ts" }],
776+
}],
777+
};
778+
779+
await handler({ message: "should be ignored" } as never, output as never);
780+
781+
assertEquals(sessionManager.activeCalls, []);
782+
assertEquals(sessionManager.state.pendingInjection, undefined);
783+
});
762784
});

src/handlers/messages.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,6 @@ const getTransformMessage = (input: unknown): string | undefined => {
2828
return typeof message === "string" ? message : undefined;
2929
};
3030

31-
const getLatestUserText = (
32-
output: MessagesTransformOutput,
33-
): string | undefined => {
34-
const lastUserEntry = output.messages
35-
.findLast((message) => message.info.role === "user");
36-
const textPart = lastUserEntry?.parts.find(isTextPart);
37-
return textPart
38-
? sanitizeMemoryInputPreservingMemoryBlocks(textPart.text)
39-
: undefined;
40-
};
41-
4231
const LEADING_INJECTED_MEMORY_BLOCK =
4332
/^(?:<session_memory\b[\s\S]*?<\/session_memory>|<memory\b[\s\S]*?<\/memory>|<persistent_memory\b[\s\S]*?<\/persistent_memory>)(?:\r?\n){0,2}/;
4433

@@ -64,6 +53,10 @@ export function createMessagesHandler(
6453
.findLast((message) => message.info.role === "user");
6554
if (!lastUserEntry) return;
6655

56+
const textPart = lastUserEntry.parts.find(isTextPart);
57+
const latestUserText = textPart?.text;
58+
if (latestUserText === undefined) return;
59+
6760
const sourceSessionID = lastUserEntry.info.sessionID;
6861

6962
try {
@@ -79,12 +72,9 @@ export function createMessagesHandler(
7972
canonicalSessionId,
8073
);
8174

82-
const textPart = lastUserEntry.parts.find(isTextPart);
83-
const latestUserText = textPart?.text;
84-
8575
const recallQuery = sanitizeMemoryInput(
8676
stripInjectedMemoryBlocks(
87-
getTransformMessage(input) ?? getLatestUserText(output) ?? "",
77+
getTransformMessage(input) ?? latestUserText,
8878
),
8979
) || undefined;
9080
const prepared = state.pendingInjection ??
@@ -93,8 +83,8 @@ export function createMessagesHandler(
9383
recallQuery,
9484
);
9585
if (!prepared) return;
86+
if (!textPart) return;
9687

97-
if (!textPart || latestUserText === undefined) return;
9888
const scrubbedUserText = scrubPromptMemoryText(latestUserText);
9989
const effectiveUserText = sanitizeMemoryInputPreservingMemoryBlocks(
10090
scrubbedUserText,

src/services/batch-drain.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,38 @@ describe("batch drain", () => {
303303
);
304304
});
305305

306+
it("avoids an extra ownership refresh before checkpointing skipped entries", async () => {
307+
const { events, drain } = await createDeps({
308+
drain: { batchSize: 2, claimHeartbeatIntervalMs: null },
309+
});
310+
const skipped = createSessionEvent("message", "assistant", {
311+
summary: "assistant chatter",
312+
body: "assistant chatter",
313+
});
314+
const drained = createSessionEvent("message", "user", {
315+
summary: "user message",
316+
body: "user message",
317+
});
318+
await events.recordEvent("session-1", "group-1", skipped);
319+
await events.recordEvent("session-1", "group-1", drained);
320+
321+
const refreshSpy = spy(events, "refreshClaimLease");
322+
const added: string[] = [];
323+
try {
324+
const result = await drain.drainGroup("group-1", {
325+
addMemory(input: { name: string }) {
326+
added.push(input.name);
327+
},
328+
} as never);
329+
330+
assertEquals(result, { status: "success", drained: 1 });
331+
assertEquals(added, [`message:${drained.id}`]);
332+
assertEquals(refreshSpy.calls.length, 4);
333+
} finally {
334+
refreshSpy.restore();
335+
}
336+
});
337+
306338
it("limits batches using serialized Graphiti episode bodies", async () => {
307339
const first = createSessionEvent("message", "user", {
308340
summary: "first",

src/services/batch-drain.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ class DrainClaimLostError extends Error {
3333
const makeBatchKey = (entries: DrainQueueEntry[]): string =>
3434
`${entries[0]?.event.id ?? "empty"}:${entries.at(-1)?.event.id ?? "empty"}`;
3535

36+
const getDrainableEntryIds = (entries: DrainQueueEntry[]): Set<string> => {
37+
const drainableEntryIds = new Set<string>();
38+
for (const entry of entries) {
39+
if (shouldDrainEntry(entry)) {
40+
drainableEntryIds.add(entry.event.id);
41+
}
42+
}
43+
return drainableEntryIds;
44+
};
45+
3646
const shouldDrainEntry = (entry: DrainQueueEntry): boolean => {
3747
const text = sanitizeMemoryInput(getSessionEventRecallText(entry.event));
3848
if (!text) return false;
@@ -135,8 +145,9 @@ export class BatchDrainService {
135145

136146
const batch = claimed.entries;
137147
const batchKey = makeBatchKey(batch);
138-
const semanticBatch = batch.filter(shouldDrainEntry);
139-
if (semanticBatch.length === 0) {
148+
const eventIds = batch.map((entry) => entry.event.id);
149+
const drainableEntryIds = getDrainableEntryIds(batch);
150+
if (drainableEntryIds.size === 0) {
140151
await this.events.markBatchSuccess(groupId, claimed.claimToken, batch);
141152
await this.redis.deleteKey(drainRetryKey(groupId, batchKey));
142153
return { status: "success", drained: 0 };
@@ -149,19 +160,7 @@ export class BatchDrainService {
149160
}
150161

151162
let lostClaim = false;
152-
const refreshClaimHeartbeat = async (): Promise<void> => {
153-
try {
154-
const refreshed = await this.events.refreshClaimLease(
155-
groupId,
156-
claimed.claimToken,
157-
claimed.lockTtlSeconds,
158-
);
159-
if (!refreshed) lostClaim = true;
160-
} catch {
161-
lostClaim = true;
162-
}
163-
};
164-
const confirmClaimOwnership = async (): Promise<boolean> => {
163+
const refreshClaimOwnership = async (): Promise<boolean> => {
165164
if (lostClaim) return false;
166165
try {
167166
const refreshed = await this.events.refreshClaimLease(
@@ -175,6 +174,11 @@ export class BatchDrainService {
175174
}
176175
return !lostClaim;
177176
};
177+
const refreshClaimHeartbeat = async (): Promise<void> => {
178+
await refreshClaimOwnership();
179+
};
180+
const confirmClaimOwnership = (): Promise<boolean> =>
181+
refreshClaimOwnership();
178182
const assertClaimOwnership = async (): Promise<void> => {
179183
if (!await confirmClaimOwnership()) {
180184
throw new DrainClaimLostError();
@@ -187,8 +191,8 @@ export class BatchDrainService {
187191

188192
try {
189193
for (const entry of batch) {
190-
await assertClaimOwnership();
191-
if (shouldDrainEntry(entry)) {
194+
if (drainableEntryIds.has(entry.event.id)) {
195+
await assertClaimOwnership();
192196
await graphiti.addMemory({
193197
name: `${entry.event.category}:${entry.event.id}`,
194198
episodeBody: entry.episodeBody,
@@ -208,13 +212,13 @@ export class BatchDrainService {
208212
await assertClaimOwnership();
209213
await this.events.markBatchSuccess(groupId, claimed.claimToken, batch);
210214
await this.redis.deleteKey(drainRetryKey(groupId, batchKey));
211-
return { status: "success", drained: semanticBatch.length };
215+
return { status: "success", drained: drainableEntryIds.size };
212216
} catch (err) {
213217
const lostOwnership = err instanceof DrainClaimLostError;
214218
if (lostOwnership) {
215219
logger.warn("Drain claim heartbeat lost ownership", {
216220
groupId,
217-
eventIds: batch.map((entry) => entry.event.id),
221+
eventIds,
218222
});
219223
}
220224
const attempts = (retryState?.attempts ?? 0) + 1;
@@ -223,7 +227,7 @@ export class BatchDrainService {
223227
if (!lostOwnership) {
224228
logger.warn("Drain claim heartbeat lost ownership", {
225229
groupId,
226-
eventIds: batch.map((entry) => entry.event.id),
230+
eventIds,
227231
});
228232
}
229233
await this.redis.deleteKey(drainRetryKey(groupId, batchKey));

src/services/connection-manager.ts

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -430,18 +430,6 @@ export class GraphitiConnectionManager implements GraphitiToolCaller {
430430
}
431431
}
432432

433-
private async executeConnectedCall(
434-
name: string,
435-
args: Record<string, unknown>,
436-
deadlineMs: number,
437-
): Promise<unknown> {
438-
return await this.executeConnectedCallWithinDeadline(
439-
name,
440-
args,
441-
this.now() + deadlineMs,
442-
);
443-
}
444-
445433
private async executeConnectedCallWithinDeadline(
446434
name: string,
447435
args: Record<string, unknown>,
@@ -470,49 +458,58 @@ export class GraphitiConnectionManager implements GraphitiToolCaller {
470458
}
471459

472460
if (isSessionExpired(err)) {
473-
const typedError = new GraphitiSessionExpiredError(
474-
getErrorMessage(err) || undefined,
475-
);
476-
477-
if (attempt >= 1) {
478-
void this.reconnect();
479-
throw typedError;
480-
}
481-
482-
const connected = await this.reconnectWithinDeadline(deadlineAt);
483-
if (!connected) throw typedError;
484-
return await this.executeConnectedCallWithinDeadline(
461+
return await this.retryConnectedCallAfterRecoverableError(
462+
new GraphitiSessionExpiredError(
463+
getErrorMessage(err) || undefined,
464+
),
485465
name,
486466
args,
487467
deadlineAt,
488-
attempt + 1,
468+
attempt,
489469
);
490470
}
491471

492472
if (isTransportFailure(err)) {
493-
const typedError = new GraphitiTransportError(
494-
getErrorMessage(err) || undefined,
495-
);
496-
497-
if (attempt >= 1) {
498-
void this.reconnect();
499-
throw typedError;
500-
}
501-
502-
const connected = await this.reconnectWithinDeadline(deadlineAt);
503-
if (!connected) throw typedError;
504-
return await this.executeConnectedCallWithinDeadline(
473+
return await this.retryConnectedCallAfterRecoverableError(
474+
new GraphitiTransportError(
475+
getErrorMessage(err) || undefined,
476+
),
505477
name,
506478
args,
507479
deadlineAt,
508-
attempt + 1,
480+
attempt,
509481
);
510482
}
511483

512484
throw err;
513485
}
514486
}
515487

488+
private async retryConnectedCallAfterRecoverableError(
489+
typedError: GraphitiSessionExpiredError | GraphitiTransportError,
490+
name: string,
491+
args: Record<string, unknown>,
492+
deadlineAt: number,
493+
attempt: number,
494+
): Promise<unknown> {
495+
if (attempt >= 1) {
496+
void this.reconnect();
497+
throw typedError;
498+
}
499+
500+
const connected = await this.reconnectWithinDeadline(deadlineAt);
501+
if (!connected) {
502+
throw typedError;
503+
}
504+
505+
return await this.executeConnectedCallWithinDeadline(
506+
name,
507+
args,
508+
deadlineAt,
509+
attempt + 1,
510+
);
511+
}
512+
516513
private getRemainingDeadlineMs(deadlineAt: number): number {
517514
return deadlineAt - this.now();
518515
}

0 commit comments

Comments
 (0)