Compare commits

...
Author SHA1 Message Date
bincxz 0d7cb4013d fix(ai): preserve user text during request compression 2026-09-07 02:29:46 +08:00
2 changed files with 22 additions and 0 deletions

No files matched your search

@@ -6,6 +6,24 @@ import { TraceStore } from './traceStore.ts';
import { ToolOutputStore } from './toolOutputStore.ts';
import { createInitialCattyRuntimeContext } from './cattyRuntimeContext.ts';
test('normal turn and step preparation preserve exact user literals alongside noisy tool output', async () => {
const literal = "printf '%s\\n' 'CANONICAL_GUI_" + 'x'.repeat(1200) + "_END'";
const content = `Execute exactly: ${literal}\n\n\n\n${'keep this line\n'.repeat(5)}`;
for (const userContent of [content, [{ type: 'text' as const, text: content }]]) {
const messages: ModelMessage[] = [
{ role: 'user', content: userContent },
{ role: 'assistant', content: [{ type: 'tool-call', toolCallId: 'noise', toolName: 'terminal_execute', input: {} }] },
{ role: 'tool', content: [{ type: 'tool-result', toolCallId: 'noise', toolName: 'terminal_execute', output: { type: 'text', value: 'log '.repeat(10000) } }] },
];
const turn = await prepareTurnContext({ messages, backend: 'catty', contextWindow: 1_300_000, trigger: 'pre-turn' });
const step = await prepareStepContext({ messages: turn.messages, contextWindow: 1_300_000, stepNumber: 0 });
for (const prepared of [turn, step]) {
assert.deepEqual(prepared.messages.find(message => message.role === 'user')?.content, userContent);
assert.ok(JSON.stringify(prepared.messages).length < JSON.stringify(messages).length);
}
}
});
test('prepareTurnContext applies typed compression before LLM summarize threshold', async () => {
const longOutput = 'line\n'.repeat(20_000);
const messages: ModelMessage[] = [
@@ -124,6 +124,9 @@ export function compressMessagesForRequestTooLargeRetry(
function compressModelMessageForRequestRetry(message: ModelMessage): ModelMessage {
if (typeof message.content === "string") {
// User text is an instruction or exact input, not terminal output. Lossy
// cleanup can alter commands even when the request is far below its limit.
if (message.role === "user") return message;
const content = compressAndTruncateText(message.content, RETRY_MAX_MESSAGE_TEXT_CHARS);
return content === message.content ? message : ({ ...message, content } as ModelMessage);
}
@@ -132,6 +135,7 @@ function compressModelMessageForRequestRetry(message: ModelMessage): ModelMessag
let didAdjust = false;
const content = message.content.map((part) => {
if (message.role === "user" && part.type === "text") return part;
const compressed = compressContentPartForRequestRetry(part);
if (compressed !== part) didAdjust = true;
return compressed;