Skip to content

Commit 824e45c

Browse files
authored
fix(cli): prevent auto compaction loop and prune messages until valid (#8535)
fix: prevent auto compaction loop and prune messages until valid
1 parent d0b042a commit 824e45c

File tree

2 files changed

+51
-33
lines changed

2 files changed

+51
-33
lines changed

extensions/cli/src/compaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function compactChatHistory(
103103
llmApi,
104104
controller,
105105
streamCallbacks,
106+
true,
106107
);
107108

108109
// Create the compacted history with a special marker

extensions/cli/src/stream/streamChatResponse.ts

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
ChatCompletionTool,
99
} from "openai/resources.mjs";
1010

11+
import { pruneLastMessage } from "../compaction.js";
1112
import { services } from "../services/index.js";
1213
import { posthogService } from "../telemetry/posthogService.js";
1314
import { telemetryService } from "../telemetry/telemetryService.js";
@@ -130,6 +131,7 @@ interface ProcessStreamingResponseOptions {
130131
}
131132

132133
// Process a single streaming response and return whether we need to continue
134+
// eslint-disable-next-line max-statements
133135
export async function processStreamingResponse(
134136
options: ProcessStreamingResponseOptions,
135137
): Promise<{
@@ -138,18 +140,22 @@ export async function processStreamingResponse(
138140
toolCalls: ToolCall[];
139141
shouldContinue: boolean;
140142
}> {
141-
const {
142-
chatHistory,
143-
model,
144-
llmApi,
145-
abortController,
146-
callbacks,
147-
isHeadless,
148-
tools,
149-
} = options;
143+
const { model, llmApi, abortController, callbacks, isHeadless, tools } =
144+
options;
145+
146+
let chatHistory = options.chatHistory;
150147

151148
// Validate context length before making the request
152-
const validation = validateContextLength(chatHistory, model);
149+
let validation = validateContextLength(chatHistory, model);
150+
151+
// Prune last messages until valid
152+
while (chatHistory.length > 1 && !validation.isValid) {
153+
const prunedChatHistory = pruneLastMessage(chatHistory);
154+
if (prunedChatHistory.length === chatHistory.length) break;
155+
chatHistory = prunedChatHistory;
156+
validation = validateContextLength(chatHistory, model);
157+
}
158+
153159
if (!validation.isValid) {
154160
throw new Error(`Context length validation failed: ${validation.error}`);
155161
}
@@ -323,13 +329,14 @@ export async function processStreamingResponse(
323329
}
324330

325331
// Main function that handles the conversation loop
326-
// eslint-disable-next-line complexity
332+
// eslint-disable-next-line complexity, max-params
327333
export async function streamChatResponse(
328334
chatHistory: ChatHistoryItem[],
329335
model: ModelConfig,
330336
llmApi: BaseLlmApi,
331337
abortController: AbortController,
332338
callbacks?: StreamCallbacks,
339+
isCompacting = false,
333340
) {
334341
logger.debug("streamChatResponse called", {
335342
model,
@@ -350,33 +357,43 @@ export async function streamChatResponse(
350357
chatHistorySvc.isReady()
351358
) {
352359
try {
353-
chatHistory = chatHistorySvc.getHistory();
360+
// use chat history from params when isCompacting is true
361+
// otherwise use the full history
362+
if (!isCompacting) {
363+
chatHistory = chatHistorySvc.getHistory();
364+
}
354365
} catch {}
355366
}
356367
logger.debug("Starting conversation iteration");
357368

358-
// Pre-API auto-compaction checkpoint
359-
const { wasCompacted: preCompacted, chatHistory: preCompactHistory } =
360-
await handleAutoCompaction(chatHistory, model, llmApi, {
361-
isHeadless,
362-
callbacks: {
363-
onSystemMessage: callbacks?.onSystemMessage,
364-
onContent: callbacks?.onContent,
365-
},
366-
});
369+
logger.debug("debug1 streamChatResponse history", { chatHistory });
370+
371+
// avoid pre-compaction when compaction is in progress
372+
// this also avoids infinite compaction call stacks
373+
if (!isCompacting) {
374+
// Pre-API auto-compaction checkpoint
375+
const { wasCompacted: preCompacted, chatHistory: preCompactHistory } =
376+
await handleAutoCompaction(chatHistory, model, llmApi, {
377+
isHeadless,
378+
callbacks: {
379+
onSystemMessage: callbacks?.onSystemMessage,
380+
onContent: callbacks?.onContent,
381+
},
382+
});
367383

368-
if (preCompacted) {
369-
logger.debug("Pre-API compaction occurred, updating chat history");
370-
// Update chat history after pre-compaction
371-
const chatHistorySvc2 = services.chatHistory;
372-
if (
373-
typeof chatHistorySvc2?.isReady === "function" &&
374-
chatHistorySvc2.isReady()
375-
) {
376-
chatHistorySvc2.setHistory(preCompactHistory);
377-
chatHistory = chatHistorySvc2.getHistory();
378-
} else {
379-
chatHistory = [...preCompactHistory];
384+
if (preCompacted) {
385+
logger.debug("Pre-API compaction occurred, updating chat history");
386+
// Update chat history after pre-compaction
387+
const chatHistorySvc2 = services.chatHistory;
388+
if (
389+
typeof chatHistorySvc2?.isReady === "function" &&
390+
chatHistorySvc2.isReady()
391+
) {
392+
chatHistorySvc2.setHistory(preCompactHistory);
393+
chatHistory = chatHistorySvc2.getHistory();
394+
} else {
395+
chatHistory = [...preCompactHistory];
396+
}
380397
}
381398
}
382399

0 commit comments

Comments
 (0)