diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 15d8155b..fe4cd393 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -1807,7 +1807,10 @@ class AgentManager: if session.needs_fork: session.needs_fork = False elif len(session.messages) > 1: - history = _build_history_prefix(_get_branch_messages(session)) + history = _build_history_prefix( + _get_branch_messages(session), + cutoff_msg_id=session.compacted_through_msg_id, + ) if history: if isinstance(prompt_content, str): prompt_content = history + "\n\n" + prompt_content diff --git a/backend/apps/agents/manager/session/history_compaction.py b/backend/apps/agents/manager/session/history_compaction.py index 2757758b..f644e2e6 100644 --- a/backend/apps/agents/manager/session/history_compaction.py +++ b/backend/apps/agents/manager/session/history_compaction.py @@ -48,8 +48,17 @@ def _get_branch_messages(session) -> list: return result -def _build_history_prefix(messages) -> str: - """Format branch messages into a conversation summary for context injection.""" +def _build_history_prefix(messages, cutoff_msg_id: str | None = None) -> str: + """Format branch messages into a conversation summary for context injection. + + When `cutoff_msg_id` is provided (session.compacted_through_msg_id), drop every + message up to and including that id so the marker the UI shows actually matches + what the model sees. Missing cutoff id falls through to full history. + """ + if cutoff_msg_id: + skip_idx = next((i for i, m in enumerate(messages) if m.id == cutoff_msg_id), -1) + if skip_idx >= 0: + messages = messages[skip_idx + 1:] lines = [] for m in messages: if m.role not in ("user", "assistant") or getattr(m, "hidden", False): diff --git a/backend/main.py b/backend/main.py index f4c76cf4..ad21f1ee 100644 --- a/backend/main.py +++ b/backend/main.py @@ -716,9 +716,10 @@ async def mcp_meta(action: str, request: Request): async def session_compact(session_id: str): """Force a compaction pass on a session (Phase 2 /compact slash cmd). - Cheap programmatic summarization (no aux LLM call), so it's safe to - invoke at any time. Sets needs_fork=True so the next turn rebuilds - options and ships the compacted prefix. + User explicitly clicked compact, so we accept the prompt-cache loss in exchange + for a real visible trim: needs_fresh_session drops the SDK convo so the next turn + rebuilds from history with compacted_through_msg_id actually applied (auto-compact + only sets the marker; the button is the user opting into the cost). """ from backend.apps.agents.agent_manager import agent_manager from backend.apps.agents.core.ws_manager import ws_manager as _ws @@ -726,7 +727,8 @@ async def session_compact(session_id: str): if not session: return JSONResponse({"error": "session not found"}, status_code=404) did_compact = agent_manager._maybe_compact(session, force=True) - session.needs_fork = True + if did_compact: + session.needs_fresh_session = True await _ws.send_to_session(session_id, "agent:context_status", { "session_id": session_id, "reason": "compacted_manual" if did_compact else "noop",