Compare commits

...

3 Commits

Author SHA1 Message Date
matt korwel 3c60f743f8 Merge branch 'main' into fix/memory-optimizations 2026-02-10 08:24:54 -06:00
mkorwel 80e9fd51eb perf: avoid double stringification in ChatRecordingService 2026-02-09 16:44:28 -06:00
mkorwel fca27bd4a4 perf: optimize chat recording and add universal tool output truncation 2026-02-09 12:41:08 -06:00
2 changed files with 33 additions and 13 deletions
+1 -2
View File
@@ -17,7 +17,6 @@ import {
logToolOutputTruncated,
runInDevTraceSpan,
} from '../index.js';
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
import { ShellToolInvocation } from '../tools/shell.js';
import { executeToolWithHooks } from '../core/coreToolHookTriggers.js';
import {
@@ -204,7 +203,7 @@ export class ToolExecutor {
const toolName = call.request.name;
const callId = call.request.callId;
if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) {
if (typeof content === 'string') {
const threshold = this.config.getTruncateToolOutputThreshold();
if (threshold > 0 && content.length > threshold) {
@@ -125,6 +125,7 @@ export interface ResumedSessionData {
*/
export class ChatRecordingService {
private conversationFile: string | null = null;
private conversation: ConversationRecord | null = null;
private cachedLastConvData: string | null = null;
private sessionId: string;
private projectHash: string;
@@ -148,6 +149,7 @@ export class ChatRecordingService {
// Resume from existing session
this.conversationFile = resumedSessionData.filePath;
this.sessionId = resumedSessionData.conversation.sessionId;
this.conversation = resumedSessionData.conversation;
// Update the session ID in the existing file
this.updateConversation((conversation) => {
@@ -174,13 +176,15 @@ export class ChatRecordingService {
)}.json`;
this.conversationFile = path.join(chatsDir, filename);
this.writeConversation({
const initialConversation: ConversationRecord = {
sessionId: this.sessionId,
projectHash: this.projectHash,
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
messages: [],
});
};
this.conversation = initialConversation;
this.writeConversation(initialConversation, { allowEmpty: true });
}
// Clear any queued data since this is a fresh start
@@ -417,9 +421,12 @@ export class ChatRecordingService {
* Loads up the conversation record from disk.
*/
private readConversation(): ConversationRecord {
if (this.conversation) return this.conversation;
try {
this.cachedLastConvData = fs.readFileSync(this.conversationFile!, 'utf8');
return JSON.parse(this.cachedLastConvData);
this.conversation = JSON.parse(this.cachedLastConvData);
return this.conversation!;
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
@@ -428,13 +435,14 @@ export class ChatRecordingService {
}
// Placeholder empty conversation if file doesn't exist.
return {
this.conversation = {
sessionId: this.sessionId,
projectHash: this.projectHash,
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
messages: [],
};
return this.conversation;
}
}
@@ -448,14 +456,27 @@ export class ChatRecordingService {
try {
if (!this.conversationFile) return;
// Don't write the file yet until there's at least one message.
if (conversation.messages.length === 0 && !allowEmpty) return;
if ((conversation.messages?.length ?? 0) === 0 && !allowEmpty) return;
// Only write the file if this change would change the file.
if (this.cachedLastConvData !== JSON.stringify(conversation, null, 2)) {
conversation.lastUpdated = new Date().toISOString();
const newContent = JSON.stringify(conversation, null, 2);
this.cachedLastConvData = newContent;
fs.writeFileSync(this.conversationFile, newContent);
// Avoid redundant stringification by checking if the content changed first.
// We use the cached string for comparison to avoid a new stringification
// when nothing has changed.
const currentContent = JSON.stringify(conversation, null, 2);
if (this.cachedLastConvData !== currentContent) {
// To avoid a second full stringification for a large conversation object,
// we can replace just the timestamp in the already stringified content.
// This is significantly more performant.
const newTimestamp = new Date().toISOString();
const finalContent = currentContent.replace(
`"lastUpdated": "${conversation.lastUpdated}"`,
`"lastUpdated": "${newTimestamp}"`,
);
// Update the in-memory object's timestamp to stay in sync.
conversation.lastUpdated = newTimestamp;
this.cachedLastConvData = finalContent;
fs.writeFileSync(this.conversationFile, finalContent);
}
} catch (error) {
// Handle disk full (ENOSPC) gracefully - disable recording but allow conversation to continue