diff --git a/.changeset/anchor-compaction-success-patterns.md b/.changeset/anchor-compaction-success-patterns.md new file mode 100644 index 000000000..c4ed66e82 --- /dev/null +++ b/.changeset/anchor-compaction-success-patterns.md @@ -0,0 +1,5 @@ +--- +"@nanocollective/nanocoder": patch +--- + +Fixed default compaction collapsing a multi-line tool result into `Result: success` whenever it mentioned "completed successfully" or "no errors" anywhere, which dropped failure lines that followed. Only a short single-line result is treated as a success marker now. Closes #1633. diff --git a/source/utils/message-compression.spec.ts b/source/utils/message-compression.spec.ts index fdc87ac8f..e16056ec9 100644 --- a/source/utils/message-compression.spec.ts +++ b/source/utils/message-compression.spec.ts @@ -464,6 +464,41 @@ test('compressMessages detects success in tool results', t => { t.true(toolMsg?.content?.includes('success') ?? false); }); +test('compressMessages keeps a log that mentions success but also reports failures', t => { + const tokenizer = createMockTokenizer(); + const messages: Message[] = [ + createToolMessage( + 'execute_bash', + 'Build completed successfully\nfailures: 3\nsee report.txt', + ), + createUserMessage('Recent'), + ]; + + const result = compressMessages(messages, tokenizer, { + mode: 'default', + keepRecentMessages: 1, + }); + + const toolMsg = result.compressedMessages[0]; + t.false(toolMsg?.content?.includes('Result: success') ?? true); +}); + +test('compressMessages treats a single "completed successfully" line as success', t => { + const tokenizer = createMockTokenizer(); + const messages: Message[] = [ + createToolMessage('execute_bash', 'Build completed successfully'), + createUserMessage('Recent'), + ]; + + const result = compressMessages(messages, tokenizer, { + mode: 'default', + keepRecentMessages: 1, + }); + + const toolMsg = result.compressedMessages[0]; + t.true(toolMsg?.content?.includes('Result: success') ?? false); +}); + // ==================== keepRecentMessages option ==================== test('compressMessages respects custom keepRecentMessages value', t => { @@ -603,3 +638,22 @@ test('compressMessages preserves display-only messages in output but excludes th ); t.is(result.compressedTokenCount, expectedCompressedTokens); }); + +test('compressMessages keeps the trailing detail of a one-line success', t => { + const tokenizer = createMockTokenizer(); + const messages: Message[] = [ + createToolMessage('execute_bash', 'Operation completed successfully in 5s'), + createUserMessage('Recent'), + ]; + + const result = compressMessages(messages, tokenizer, { + mode: 'default', + keepRecentMessages: 1, + }); + + const toolMsg = result.compressedMessages[0]; + t.is( + toolMsg?.content, + 'Tool: execute_bash\nResult: Operation completed successfully in 5s', + ); +}); diff --git a/source/utils/message-compression.ts b/source/utils/message-compression.ts index f769b42ab..3b42008ce 100644 --- a/source/utils/message-compression.ts +++ b/source/utils/message-compression.ts @@ -368,16 +368,23 @@ function extractErrorInfo(content: string): { return null; } -// Check if content indicates success +// Check if content indicates success. +// Only a short, single-line result counts: a multi-line log that happens to +// contain "completed successfully" may also report failures further down, +// so it must not be collapsed into "Result: success" (#1633). function isSuccess(content: string): boolean { + const text = content.trim(); + if (text.includes('\n')) { + return false; + } const successPatterns = [ /^success$/i, /^ok$/i, /^done$/i, - /completed successfully/i, - /no errors/i, + /^.{0,80}completed successfully\.?$/i, + /^no errors\.?$/i, ]; - return successPatterns.some(pattern => pattern.test(content)); + return successPatterns.some(pattern => pattern.test(text)); } // Check for error in content