|
45 | 45 | from api.usage import prompt_cache_hit_percent |
46 | 46 | from api.models import ( |
47 | 47 | _is_empty_partial_activity_message, |
| 48 | + _message_timestamp_as_float, |
48 | 49 | get_state_db_session_messages, |
49 | 50 | reconciled_state_db_messages_for_session, |
50 | 51 | ) |
@@ -2618,6 +2619,32 @@ def _restore_display_reasoning_metadata(previous_messages, updated_messages): |
2618 | 2619 | return updated_messages |
2619 | 2620 |
|
2620 | 2621 |
|
| 2622 | +def _clamp_context_to_watermark(session, messages: list) -> list: |
| 2623 | + """Filter context_messages to the truncation watermark boundary (#2914). |
| 2624 | +
|
| 2625 | + When a user edits, regenerates, or undoes messages, the agent's result |
| 2626 | + may contain the full state.db history including turns the user deliberately |
| 2627 | + removed. This helper drops messages whose timestamp exceeds the active |
| 2628 | + watermark so the next turn doesn't feed the agent "deleted" rows again. |
| 2629 | + """ |
| 2630 | + _tw = getattr(session, 'truncation_watermark', None) |
| 2631 | + if _tw is None: |
| 2632 | + return messages |
| 2633 | + _tw_ts = _message_timestamp_as_float({'timestamp': _tw}) |
| 2634 | + if _tw_ts is None: |
| 2635 | + return messages |
| 2636 | + _clamped = [ |
| 2637 | + m for m in messages |
| 2638 | + if (m_ts := _message_timestamp_as_float(m)) is None or m_ts <= _tw_ts |
| 2639 | + ] |
| 2640 | + if len(_clamped) != len(messages): |
| 2641 | + logger.info( |
| 2642 | + "clamping context_messages: %d → %d (watermark=%.2f, session=%s)", |
| 2643 | + len(messages), len(_clamped), _tw_ts, session.session_id, |
| 2644 | + ) |
| 2645 | + return _clamped |
| 2646 | + |
| 2647 | + |
2621 | 2648 | def _session_context_messages(session): |
2622 | 2649 | """Return model-facing history without assuming it matches the UI transcript.""" |
2623 | 2650 | context_messages = getattr(session, 'context_messages', None) |
@@ -5148,6 +5175,7 @@ def _periodic_checkpoint(): |
5148 | 5175 | _previous_context_messages, |
5149 | 5176 | _next_context_messages, |
5150 | 5177 | ) |
| 5178 | + _next_context_messages = _clamp_context_to_watermark(s, _next_context_messages) |
5151 | 5179 | s.context_messages = _deduplicate_context_messages(_next_context_messages) |
5152 | 5180 | s.messages = _merge_display_messages_after_agent_result( |
5153 | 5181 | _previous_messages, |
@@ -5295,6 +5323,7 @@ def _periodic_checkpoint(): |
5295 | 5323 | _previous_context_messages, |
5296 | 5324 | _next_context_messages, |
5297 | 5325 | ) |
| 5326 | + _next_context_messages = _clamp_context_to_watermark(s, _next_context_messages) |
5298 | 5327 | s.context_messages = _deduplicate_context_messages(_next_context_messages) |
5299 | 5328 | s.messages = _merge_display_messages_after_agent_result( |
5300 | 5329 | _previous_messages, |
@@ -6172,6 +6201,7 @@ def _periodic_checkpoint(): |
6172 | 6201 | _previous_context_messages, |
6173 | 6202 | _next_context_messages, |
6174 | 6203 | ) |
| 6204 | + _next_context_messages = _clamp_context_to_watermark(s, _next_context_messages) |
6175 | 6205 | s.context_messages = _deduplicate_context_messages(_next_context_messages) |
6176 | 6206 | s.messages = _merge_display_messages_after_agent_result( |
6177 | 6207 | _previous_messages, |
|
0 commit comments