Files
hermes-agent/agent/conversation_compression_codex.py
mnajafian-nv c3af207f28 fix(compression): keep a late-unwinding attempt's own id, session and trigger
The attempt record read its identity from the agent's current-attempt fields,
which the next attempt overwrites when it begins. A stalled attempt that
unwound after its stall fallback had started therefore logged the fallback's
attempt_id, session_id and trigger_source, so two records claimed one attempt
and the stalled one disappeared.

Each attempt now carries its own seed on _Attempt and passes it to every
emit, so the record always describes the attempt that wrote it. Emits with no
attempt in hand (pool saturation) keep reading the agent's copy.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit a36db44d63888406702cab44d3ca29335727a6e2)
2026-10-10 04:27:27 -07:00

137 lines
7.2 KiB
Python

"""Codex app-server compaction: the route ``compress_context`` takes for Codex-owned threads.
Split from ``agent/conversation_compression.py`` (facade size cap), which imports it at module level;
this module must never import the facade at module level (import cycle), so it late-imports the
facade's shared helpers inside functions. It logs under the facade's logger name so log consumers keep
one source.
"""
from __future__ import annotations
import contextlib
import logging
from typing import Any, Optional
from agent.conversation_compression_telemetry import _emit_bypassed_attempt_telemetry
logger = logging.getLogger("agent.conversation_compression")
def _codex_compaction_cooldown_remaining(agent: Any) -> float:
"""Seconds left on this session's compaction-failure cooldown (0 = clear)."""
compressor = getattr(agent, "context_compressor", None)
getter = getattr(compressor, "get_active_compression_failure_cooldown", None)
if not callable(getter):
return 0.0
try:
state = getter(refresh=True)
except Exception:
logger.debug("codex compaction cooldown lookup failed", exc_info=True)
return 0.0
try:
return max(0.0, float(state.get("remaining_seconds") or 0.0)) if state else 0.0
except (TypeError, ValueError):
return 0.0
def _record_codex_compaction_failure(agent: Any, error: str) -> None:
"""Arm the shared compression-failure cooldown after a failed codex compaction.
The codex path returns the transcript unchanged, so without a cooldown the still-over-threshold session
would retry every turn."""
from agent.context_compressor import _SUMMARY_FAILURE_COOLDOWN_SECONDS
from agent.conversation_compression import _swallow
compressor = getattr(agent, "context_compressor", None)
recorder = getattr(compressor, "_record_compression_failure_cooldown", None)
if not callable(recorder):
return
with _swallow('codex compaction cooldown persist failed', exc_info=True):
recorder(_SUMMARY_FAILURE_COOLDOWN_SECONDS, error)
def _compress_context_via_codex_app_server(
agent: Any, messages: list, system_message: Optional[str], *, approx_tokens: Optional[int] = None,
task_id: str = "default", force: bool = False, started_at: float, attempt_seed: dict[str, Any],
) -> tuple[list, str]:
"""Route compaction to Codex app-server for Codex-owned threads.
Rewriting the local transcript would not shrink the Codex thread, so Codex compacts its own thread and
Hermes' transcript is left unchanged. Every exit, raising ones included, logs one attempt record."""
from agent.conversation_compression import (
COMPACTION_STATUS, _CompressionActivityHeartbeat, _emit_compaction_done, _existing_system_prompt,
_reset_read_dedup_caches, _swallow,
)
def _record(commit_status: str, failure_class: Optional[str], method: str = "none") -> None:
_emit_bypassed_attempt_telemetry(
agent, started_at, commit_status=commit_status, failure_class=failure_class, approx_tokens=approx_tokens,
route="codex_app_server", method=method, attempt_seed=attempt_seed,
)
_sid = getattr(agent, "session_id", None) or "none"
_tokens = f"{approx_tokens:,}" if approx_tokens else "unknown"
auto_mode = str(getattr(agent, "codex_app_server_auto_compaction", "native") or "native").lower()
if auto_mode not in {"native", "hermes", "off"}:
auto_mode = "native"
skip_reason = skip_status = skip_class = None
if not force and auto_mode != "hermes":
skip_reason, skip_status, skip_class = f"mode={auto_mode} force=false", "skipped", f"codex_auto_{auto_mode}"
elif not force:
# Automatic entrypoints honor the compressor-owned cooldown: a recent compaction
# failed, and retrying every turn is what thrashes.
_cooldown_remaining = _codex_compaction_cooldown_remaining(agent)
if _cooldown_remaining > 0:
skip_reason = f"failure cooldown active for {_cooldown_remaining:.0f}s"
skip_status, skip_class = "blocked", "blocked:cooldown"
codex_session = getattr(agent, "_codex_session", None)
if skip_reason is None and codex_session is None:
skip_reason, skip_status, skip_class = "no active codex thread", "skipped", "codex_no_thread"
if skip_reason is not None:
logger.info(
"codex app-server compaction skipped: %s (session=%s messages=%d tokens=~%s)", skip_reason, _sid,
len(messages), _tokens,
)
_record(skip_status, skip_class)
return messages, _existing_system_prompt(agent, system_message)
logger.info("codex app-server compaction started: session=%s messages=%d tokens=~%s", _sid, len(messages), _tokens)
with contextlib.suppress(Exception):
agent._emit_status(COMPACTION_STATUS)
_activity_heartbeat = _CompressionActivityHeartbeat(agent, emit_client_status=True).start()
try:
result = codex_session.compact_thread()
except BaseException as exc:
_activity_heartbeat.stop("context compression failed")
# Same class shape as the Hermes route's raising exits; the caller still gets the original error.
_record("aborted", f"exception:{type(exc).__name__}")
raise
failed = bool(getattr(result, "interrupted", False) or getattr(result, "error", None))
_activity_heartbeat.stop("context compression failed" if failed else "context compression completed")
if getattr(result, "should_retire", False):
with contextlib.suppress(Exception):
codex_session.close()
agent._codex_session = None
if failed:
with contextlib.suppress(Exception):
agent._emit_warning(f"⚠ Codex app-server compaction failed: {result.error}")
# The transcript is returned unchanged, so the session is still over
# threshold. Without a brake the next turn retries immediately.
_record_codex_compaction_failure(agent, str(getattr(result, "error", None) or "compaction interrupted"))
_record("failed", "codex_compaction_failed")
return messages, _existing_system_prompt(agent, system_message)
with _swallow('codex compaction bookkeeping failed', exc_info=True):
from agent.codex_runtime import _record_codex_app_server_compaction, _record_codex_app_server_usage
_record_codex_app_server_compaction(agent, result, approx_tokens=approx_tokens, force=True)
# An empty usage report must consume the pending verdict, not leave deferral
# armed until a later turn; minimal test engines may lack update_from_response.
if hasattr(agent.context_compressor, "update_from_response"):
_record_codex_app_server_usage(agent, result, messages=messages)
_reset_read_dedup_caches(task_id, session_id=agent.session_id or "")
logger.info(
"codex app-server compaction done: session=%s thread=%s turn=%s", _sid,
getattr(result, "thread_id", None) or "", getattr(result, "turn_id", None) or "",
)
existing_prompt = _existing_system_prompt(agent, system_message)
_record("committed", None, method="provider")
# Terminal edge only on success — failure/interrupt paths above return
# without it, matching the main compress_context() gating.
_emit_compaction_done(agent)
return messages, existing_prompt