"""Content-free ``hermes.compaction`` v1 records for every history rewrite, and their fan-out. One record per ``compress_context`` attempt (built from the attempt telemetry seam), per micro-compaction pass, per committed proactive tool-result prune, and per Codex-native thread compaction Hermes observes. A persistence-detached fork (background review) shares the live session id but rewrites only its own throwaway transcript, so it publishes nothing. The payload is flat because OpenTelemetry exporters flatten only top-level keys; every categorical field except the free-form ``summarizer_provider`` / ``summarizer_model`` identifiers comes from a closed set with an ``other`` fallback; no field carries message, summary, focus-topic, error or path text. Publishing never raises into compaction. A committed record also fires the ``post_compaction`` plugin hook, so plugins learn about every compaction the model will see without inferring it from history shrinkage. """ from __future__ import annotations import functools import logging import threading import uuid from typing import Any from agent.model_metadata import estimate_messages_tokens_rough logger = logging.getLogger(__name__) SCHEMA_NAME = "hermes.compaction" SCHEMA_VERSION = "1" # Relay's reserved mark name: it resets the owning agent scope's LLM-history freshness, so it is used only # for a committed rewrite of what the model will see next. Everything else is an ordinary attempt mark. COMPACTION_MARK = "compaction" ATTEMPT_MARK = "compaction.attempt" # The v1 sets list only values this module emits. Later versions may add request-scope truncation kinds. _KINDS = frozenset({"summarize", "micro_summarize", "prune_tool_results", "provider_native"}) _SCOPES = frozenset({"history", "provider"}) _METHODS = frozenset({ "llm_summary", "aux_fallback_main", "deterministic_fallback", "deterministic_prune", "provider", "none", }) _TRIGGERS = frozenset({ "manual", "turn_start_threshold", "pre_api", "post_tool", "idle", "engine_preflight", "overflow", "gateway_hygiene", "between_turns", "proactive_prune", "provider", "unknown", }) # Every other known trigger is automatic; an unknown or unlisted one asserts nothing. _TRIGGER_CLASSES = { "manual": "manual", "overflow": "overflow", "provider": "provider", "unknown": "unknown", "other": "unknown", } _TRIGGER_CLASS_VALUES = frozenset({"auto", "manual", "overflow", "provider", "unknown"}) # The error-classifier reasons whose recovery compresses with ``trigger="overflow"``. _OVERFLOW_REASONS = frozenset({"context_overflow", "payload_too_large", "long_context_tier"}) _OUTCOMES = frozenset({"committed", "aborted", "failed", "skipped", "blocked"}) _SPLIT_STATUSES = frozenset({ "not_applicable", "in_place_committed", "rotated_committed", "failed_not_indexed", "aborted", }) _TOKEN_COUNT_METHODS = frozenset({"estimate_rough"}) # Attempt classes beyond the shared-metrics closed set: guards, Codex route exits, lease races, an engine's # empty transcript, and the micro-compaction pass outcomes. _EXTRA_FAILURE_CLASSES = frozenset({ "blocked:cooldown", "blocked:structural_backoff", "blocked:ineffective", "blocked:unknown", "codex_auto_native", "codex_auto_off", "codex_no_thread", "codex_compaction_failed", "session_ownership_lost", "session_ownership_unreadable", "cooldown_state_unreadable", "empty_transcript", "summary_model_benched", "summarize_failed", "exchange_skipped", "defrag_failed", "stale_generation", "watermark_unavailable", }) # An aborted attempt the compressor ran but deliberately did not commit; other aborts are failures. _ABORTED_CLASSES = frozenset({"would_grow", "no_progress", "commit_fence_cancelled"}) _COMMIT_MODES = frozenset({"in_place_committed", "rotated_committed"}) _MICRO_OUTCOMES = { "absorbed": "committed", "defrag": "committed", "stale_generation": "skipped", "watermark_unavailable": "skipped", } # Every v1 key, so consumers see a stable shape; unknown values stay null. _FIELDS = ( "harness", "kind", "scope", "official", "method", "trigger", "trigger_class", "overflow_reason", "outcome", "failure_class", "attempt_id", "session_id", "tokens_before", "tokens_after", "tokens_reclaimed", "token_count_method", "messages_before", "messages_after", "items_dropped", "context_limit", "threshold_tokens", "protected_head_tokens", "middle_window_tokens", "protected_tail_tokens", "duration_ms", "summary_generation_ms", "aux_call_duration_ms", "queue_wait_ms", "commit_ms", "summarizer_provider", "summarizer_model", "tool_results_pruned", "reasoning_items_pruned", "summary_input_omitted_chars", "compression_count", "in_place", "session_rotated", "split_status", "cache_break", "has_focus_topic", ) # Integer fields copied as-is from the attempt record (record key -> payload key). _ATTEMPT_INTS = { "tokens_before": "tokens_before", "tokens_after": "tokens_after", "tokens_reclaimed": "tokens_reclaimed", "messages_before": "messages_before", "messages_after": "messages_after", "items_dropped": "items_dropped", "main_context_limit": "context_limit", "effective_threshold": "threshold_tokens", "protected_head_tokens": "protected_head_tokens", "middle_window_tokens": "middle_window_tokens", "protected_tail_tokens": "protected_tail_tokens", "total_duration_ms": "duration_ms", "summary_generation_ms": "summary_generation_ms", "aux_call_duration_ms": "aux_call_duration_ms", "queue_wait_ms": "queue_wait_ms", "commit_ms": "commit_ms", "tool_results_pruned": "tool_results_pruned", "reasoning_items_pruned": "reasoning_items_pruned", "summary_input_omitted_chars": "summary_input_omitted_chars", } _warned_lock = threading.Lock() _warned = False @functools.cache def _failure_classes() -> frozenset[str]: from hermes_cli.observability.shared_metrics_contract import COMPRESSION_FAILURE_CLASSES return COMPRESSION_FAILURE_CLASSES | _EXTRA_FAILURE_CLASSES @functools.cache def _skip_classes() -> frozenset[str]: from hermes_cli.observability.shared_metrics_contract import COMPRESSION_SKIP_CLASSES return COMPRESSION_SKIP_CLASSES def _closed(value: Any, allowed: frozenset[str], default: str | None = "other") -> str | None: if not isinstance(value, str) or not value: return None return value if value in allowed else default def _int(value: Any) -> int | None: return value if isinstance(value, int) and not isinstance(value, bool) else None def failure_class(value: Any) -> str: """Closed failure class: ``exception:`` / ``rollback:`` keep only the prefix (the type name may be a plugin's); a missing class reads ``none``.""" if not isinstance(value, str) or not value: return "none" if value.startswith(("exception:", "rollback:")): value = value.split(":", 1)[0] return value if value in _failure_classes() else "other" def event_name(payload: dict[str, Any]) -> str: """``compaction`` only for a committed history or provider rewrite; ``compaction.attempt`` otherwise.""" committed = payload.get("outcome") == "committed" and payload.get("scope") in {"history", "provider"} return COMPACTION_MARK if committed else ATTEMPT_MARK def _payload(**values: Any) -> dict[str, Any]: payload = dict.fromkeys(_FIELDS) payload.update(values, harness="hermes") payload["kind"] = _closed(payload["kind"], _KINDS) payload["scope"] = _closed(payload["scope"], _SCOPES) payload["method"] = _closed(payload["method"], _METHODS) or "none" payload["trigger"] = _closed(payload["trigger"], _TRIGGERS) or "unknown" payload["trigger_class"] = ( _closed(payload["trigger_class"], _TRIGGER_CLASS_VALUES, None) or _TRIGGER_CLASSES.get(payload["trigger"], "auto") ) payload["outcome"] = _closed(payload["outcome"], _OUTCOMES) or "other" payload["token_count_method"] = _closed(payload["token_count_method"], _TOKEN_COUNT_METHODS) payload["attempt_id"] = payload["attempt_id"] or uuid.uuid4().hex payload["session_id"] = payload["session_id"] or "" if payload["cache_break"] is None and payload["scope"] == "history": payload["cache_break"] = payload["outcome"] == "committed" return payload def _attempt_outcome(commit_status: Any, failure: str, history_rewritten: Any) -> str: # A split that failed after publishing the compacted history still rewrote what the model sees next: it is # a committed compaction whose failure_class records the failure. if history_rewritten is True: return "committed" if commit_status != "aborted": return commit_status if commit_status in _OUTCOMES else "other" if failure in _skip_classes(): return "skipped" return "aborted" if failure in _ABORTED_CLASSES else "failed" def _attempt_trigger(trigger_source: Any) -> tuple[Any, str | None]: """``(trigger, trigger_class)``. ``auto`` is what an unlabelled automatic caller records: the reason is unknown, but the attempt is still known to be automatic.""" if trigger_source == "auto": return "unknown", "auto" return trigger_source or "unknown", None def attempt_payload(record: dict[str, Any], *, compression_count: Any = None) -> dict[str, Any]: """Map one attempt telemetry record (the attempt log line's dict) onto the v1 schema.""" codex = record.get("route") == "codex_app_server" failure = failure_class(record.get("failure_class")) split_status = _closed(record.get("split_status"), _SPLIT_STATUSES) outcome = _attempt_outcome(record.get("commit_status"), failure, record.get("history_rewritten")) trigger, trigger_class = _attempt_trigger(record.get("trigger_source")) values: dict[str, Any] = {key: _int(record.get(src)) for src, key in _ATTEMPT_INTS.items()} values.update( kind="provider_native" if codex else "summarize", scope="provider" if codex else "history", official=True, method=record.get("method"), trigger=trigger, trigger_class=trigger_class, overflow_reason=_closed(record.get("overflow_reason"), _OVERFLOW_REASONS), outcome=outcome, failure_class=failure, attempt_id=record.get("attempt_id"), session_id=record.get("session_id"), token_count_method=record.get("token_count_method"), summarizer_provider=record.get("aux_provider") or None, summarizer_model=record.get("aux_model") or None, compression_count=_int(compression_count), split_status=split_status, # Commit mode is known only for a fully committed split; anything else leaves both null. in_place=split_status == "in_place_committed" if split_status in _COMMIT_MODES else None, session_rotated=split_status == "rotated_committed" if split_status in _COMMIT_MODES else None, has_focus_topic=record.get("has_focus_topic") if isinstance(record.get("has_focus_topic"), bool) else None, ) if codex: values["cache_break"] = None # the Codex thread's provider cache is not Hermes's prompt cache return _payload(**values) def micro_payload(record: dict[str, Any]) -> dict[str, Any]: """Map one micro-compaction telemetry record onto the v1 schema.""" outcome = _MICRO_OUTCOMES.get(record.get("outcome"), "failed") before, after = _int(record.get("tokens_before")), _int(record.get("tokens_after")) return _payload( kind="micro_summarize", scope="history", official=False, trigger="between_turns", outcome=outcome, method="llm_summary" if outcome == "committed" else "none", failure_class="none" if outcome == "committed" else failure_class(record.get("outcome")), session_id=record.get("session_id"), tokens_before=before, tokens_after=after, tokens_reclaimed=before - after if before is not None and after is not None else None, token_count_method="estimate_rough", messages_before=_int(record.get("messages_before")), messages_after=_int(record.get("messages_after")), duration_ms=_int(record.get("duration_ms")), threshold_tokens=_int(record.get("threshold_tokens")), context_limit=_int(record.get("context_limit")), summarizer_model=record.get("aux_model") or None, ) def prune_payload( *, session_id: str, tokens_before: int, tokens_after: int, messages: int, tool_results_pruned: int, ) -> dict[str, Any]: """A committed proactive tool-result prune: deterministic, no LLM, message count unchanged.""" return _payload( kind="prune_tool_results", scope="history", official=False, trigger="proactive_prune", outcome="committed", method="deterministic_prune", failure_class="none", session_id=session_id, tokens_before=tokens_before, tokens_after=tokens_after, tokens_reclaimed=tokens_before - tokens_after, token_count_method="estimate_rough", messages_before=messages, messages_after=messages, tool_results_pruned=tool_results_pruned, ) def provider_native_payload(*, session_id: str, compression_count: Any = None) -> dict[str, Any]: """A compaction the provider ran on its own thread and Hermes only observed.""" return _payload( kind="provider_native", scope="provider", official=False, trigger="provider", outcome="committed", method="provider", failure_class="none", session_id=session_id, compression_count=_int(compression_count), ) def fire_post_compaction( *, session_id: str, old_session_id: str = "", kind: str | None, reason: str | None, before_tokens: int | None, after_tokens: int | None, messages_removed: int | None, ) -> None: """The ``post_compaction`` plugin hook, gated on a listener. Callbacks are isolated by the dispatcher.""" from hermes_cli.lifecycle import has_hook, invoke_hook if has_hook("post_compaction"): invoke_hook( "post_compaction", session_id=session_id, old_session_id=old_session_id, kind=kind, reason=reason, before_tokens=before_tokens, after_tokens=after_tokens, messages_removed=messages_removed, ) def _fire_for(payload: dict[str, Any], session_id: str) -> None: before, after = payload["messages_before"], payload["messages_after"] fire_post_compaction( session_id=session_id, old_session_id=payload["session_id"] if payload["session_id"] != session_id else "", kind=payload["kind"], reason=payload["trigger"], before_tokens=payload["tokens_before"], after_tokens=payload["tokens_after"], messages_removed=before - after if before is not None and after is not None else None, ) def _publish( build: Any, *args: Any, turn_session_id: str | None = None, require_target_session_id: str | None = None, live_session_id: str | None = None, **kwargs: Any, ) -> None: """Build one record, fire ``post_compaction`` for a committed one, and fan it out to the built-in Relay integration. ``live_session_id`` is the session holding the compacted transcript now (a rotating commit moved it off the record's session). Never raises into compaction: the first failure logs at WARNING with its traceback, later ones at DEBUG.""" global _warned try: relay = True if require_target_session_id is not None: from agent.relay_compaction import compaction_target_available relay = compaction_target_available(require_target_session_id, turn_session_id=turn_session_id) from hermes_cli.lifecycle import has_hook hook = has_hook("post_compaction") if not (relay or hook): return payload = build(*args, **kwargs) name = event_name(payload) if hook and name == COMPACTION_MARK: _fire_for(payload, live_session_id or payload["session_id"]) if not relay: return from agent.relay_compaction import emit_compaction_mark emit_compaction_mark(payload["session_id"], name, payload, turn_session_id=turn_session_id) except Exception: with _warned_lock: first, _warned = not _warned, True logger.log(logging.WARNING if first else logging.DEBUG, "compaction event publish failed", exc_info=True) def _publishes_for(agent: Any) -> bool: """A persistence-detached fork (background review) rewrites only its own throwaway transcript.""" return not getattr(agent, "_persist_disabled", False) def _turn_session_id(agent: Any) -> str | None: """The session the agent's current turn started in; a rotating commit moves ``agent.session_id`` off it mid-turn. ``None`` outside a turn.""" value = getattr(agent, "_inflight_turn_session_id", None) return value if isinstance(value, str) and value else None def publish_attempt(agent: Any, record: dict[str, Any]) -> None: """Publish one ``compress_context`` attempt record (the attempt telemetry seam calls this).""" if _publishes_for(agent): _publish(lambda: attempt_payload( record, compression_count=getattr(getattr(agent, "context_compressor", None), "compression_count", None), ), turn_session_id=_turn_session_id(agent), live_session_id=getattr(agent, "session_id", None)) def publish_micro(record: dict[str, Any], *, turn_session_id: str | None = None) -> None: """Publish one micro-compaction pass record. The finalizer never runs micro-compaction for a fork, and passes the session its turn started in (``_turn_session_id``) because the pass may run in a rotated child.""" _publish(micro_payload, record, turn_session_id=turn_session_id) def publish_prune(agent: Any, messages: list, pruned: list, tool_results_pruned: int) -> None: """Publish one committed proactive tool-result prune, measured like the attempt record (rough estimate of the message list the model sees next).""" session_id = getattr(agent, "session_id", None) or "" turn_session_id = _turn_session_id(agent) if _publishes_for(agent): _publish(lambda: prune_payload( session_id=session_id, tokens_before=estimate_messages_tokens_rough(messages), tokens_after=estimate_messages_tokens_rough(pruned), messages=len(pruned), tool_results_pruned=tool_results_pruned, ), turn_session_id=turn_session_id, require_target_session_id=session_id) def publish_provider_native(agent: Any) -> None: """Publish one provider-native compaction Hermes observed but did not run.""" if _publishes_for(agent): _publish( provider_native_payload, session_id=getattr(agent, "session_id", None) or "", compression_count=getattr(getattr(agent, "context_compressor", None), "compression_count", None), turn_session_id=_turn_session_id(agent), )