From 4227fd5996096887757de90ba22760ac9f307b30 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 9 Dec 2025 02:45:24 +0100 Subject: [PATCH] Fix auto-compaction TUI integration and cut point logic - Trigger auto-compaction after agent_end instead of during message_end - Show CompactionComponent after auto-compaction (same as manual /compact) - Fix cut point to include bash executions before kept user message - Stop backward scan at compaction, assistant, user, or toolResult boundaries --- .../coding-agent/src/core/agent-session.ts | 35 +++++++++---------- packages/coding-agent/src/core/compaction.ts | 25 ++++++++++++- .../src/modes/interactive/interactive-mode.ts | 6 +++- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e394a1ae..be31fc90 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -135,6 +135,9 @@ export class AgentSession { } } + // Track last assistant message for auto-compaction check + private _lastAssistantMessage: AssistantMessage | null = null; + /** Internal handler for agent events - shared by subscribe and reconnect */ private _handleAgentEvent = async (event: AgentEvent): Promise => { // Notify all listeners @@ -149,11 +152,18 @@ export class AgentSession { this.sessionManager.startSession(this.agent.state); } - // Check auto-compaction after assistant messages + // Track assistant message for auto-compaction (checked on agent_end) if (event.message.role === "assistant") { - await this._runAutoCompaction(); + this._lastAssistantMessage = event.message as AssistantMessage; } } + + // Check auto-compaction after agent completes (after agent_end clears UI) + if (event.type === "agent_end" && this._lastAssistantMessage) { + const msg = this._lastAssistantMessage; + this._lastAssistantMessage = null; + this._runAutoCompaction(msg).catch(() => {}); + } }; /** @@ -584,26 +594,14 @@ export class AgentSession { * Internal: Run auto-compaction with events. * Called after assistant messages complete. */ - private async _runAutoCompaction(): Promise { + private async _runAutoCompaction(assistantMessage: AssistantMessage): Promise { const settings = this.settingsManager.getCompactionSettings(); if (!settings.enabled) return; - // Get last non-aborted assistant message - const messages = this.messages; - let lastAssistant: AssistantMessage | null = null; - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant") { - const assistantMsg = msg as AssistantMessage; - if (assistantMsg.stopReason !== "aborted") { - lastAssistant = assistantMsg; - break; - } - } - } - if (!lastAssistant) return; + // Skip if message was aborted + if (assistantMessage.stopReason === "aborted") return; - const contextTokens = calculateContextTokens(lastAssistant.usage); + const contextTokens = calculateContextTokens(assistantMessage.usage); const contextWindow = this.model?.contextWindow ?? 0; if (!shouldCompact(contextTokens, contextWindow, settings)) return; @@ -624,6 +622,7 @@ export class AgentSession { return; } + // Load entries (sync file read) then yield to let UI render const entries = this.sessionManager.loadEntries(); const compactionEntry = await compact( entries, diff --git a/packages/coding-agent/src/core/compaction.ts b/packages/coding-agent/src/core/compaction.ts index 6acb3ff7..3037f88c 100644 --- a/packages/coding-agent/src/core/compaction.ts +++ b/packages/coding-agent/src/core/compaction.ts @@ -94,7 +94,11 @@ function findTurnBoundaries(entries: SessionEntry[], startIndex: number, endInde /** * Find the cut point in session entries that keeps approximately `keepRecentTokens`. - * Returns the entry index of the first message to keep (a user message for turn integrity). + * Returns the entry index of the first entry to keep. + * + * The cut point targets a user message (turn boundary), but then scans backwards + * to include any preceding non-turn entries (bash executions, settings changes, etc.) + * that should logically be part of the kept context. * * Only considers entries between `startIndex` and `endIndex` (exclusive). */ @@ -150,6 +154,25 @@ export function findCutPoint( } } + // Scan backwards from cutIndex to include any non-turn entries (bash, settings, etc.) + // that should logically be part of the kept context + while (cutIndex > startIndex) { + const prevEntry = entries[cutIndex - 1]; + // Stop at compaction boundaries + if (prevEntry.type === "compaction") { + break; + } + if (prevEntry.type === "message") { + const role = prevEntry.message.role; + // Stop if we hit an assistant, user, or tool result (all part of previous turn) + if (role === "assistant" || role === "user" || role === "toolResult") { + break; + } + } + // Include this non-turn entry (bash, settings change, etc.) + cutIndex--; + } + return cutIndex; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c6c06095..e73af0bb 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -597,7 +597,11 @@ export class InteractiveMode { // Rebuild chat to show compacted state this.chatContainer.clear(); this.rebuildChatFromMessages(); - this.showStatus(`Auto-compacted: ${event.result.tokensBefore.toLocaleString()} tokens`); + // Add compaction component (same as manual /compact) + const compactionComponent = new CompactionComponent(event.result.tokensBefore, event.result.summary); + compactionComponent.setExpanded(this.toolOutputExpanded); + this.chatContainer.addChild(compactionComponent); + this.footer.updateState(this.session.state); } this.ui.requestRender(); break;