#!/usr/bin/env python3 """Summarize micro-compaction telemetry from Hermes logs. Reads the content-free JSON lines emitted by ``ContextCompressor._emit_micro_compaction_telemetry`` and reports what the feature actually bought you. Usage: python scripts/micro_compaction_report.py [LOGFILE ...] python scripts/micro_compaction_report.py --per-session With no LOGFILE, reads ``$HERMES_HOME/logs/agent.log`` (default ~/.hermes). What to look at --------------- The point of micro-compaction is not saving tokens or time. It is: (a) amortizing the one long batch-compaction pause across many turns, and (b) keeping the context window low enough that a session runs much further before it needs a hard compaction at all. So the headline numbers here are OCCUPANCY (how full the window is kept, as a percentage of the compaction threshold) and BATCH COMPACTIONS (how often the long pause actually fired). Net tokens saved is reported too, but it is the least interesting figure -- a session can save nothing on paper and still be a clear win because the stalls disappeared and the window never filled. Caveat: running the test suite writes telemetry into the same log. Test lines cluster inside a sub-second window and carry an empty session_id (they group as "(unknown)"). Use --per-session to spot them. """ from __future__ import annotations import argparse import json import os import sys from collections import defaultdict from pathlib import Path MICRO_MARKER = "micro compaction telemetry: " BATCH_MARKER = "context compression attempt telemetry: " def default_log() -> Path: home = os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes") return Path(home) / "logs" / "agent.log" def load(paths: list[Path]) -> tuple[list[dict], list[dict]]: micro: list[dict] = [] batch: list[dict] = [] for path in paths: try: text = path.read_text(encoding="utf-8", errors="replace") except OSError as exc: print(f"warning: cannot read {path}: {exc}", file=sys.stderr) continue for line in text.splitlines(): for marker, sink in ((MICRO_MARKER, micro), (BATCH_MARKER, batch)): idx = line.find(marker) if idx == -1: continue try: sink.append(json.loads(line[idx + len(marker):])) except ValueError: pass break return micro, batch def pct(values: list[float]) -> tuple[float, float, float] | None: if not values: return None ordered = sorted(values) return ordered[0], ordered[len(ordered) // 2], ordered[-1] def fmt(n) -> str: return "-" if n is None else f"{n:,}" def report(micro: list[dict], batch: list[dict], per_session: bool) -> int: if not micro: print("No micro-compaction telemetry found.") print("It may be disabled (compression.micro_compact), or no session") print("has run long enough to trigger a pass yet.") return 1 by_session: dict[str, list[dict]] = defaultdict(list) for e in micro: by_session[e.get("session_id") or "(unknown)"].append(e) outcomes: dict[str, int] = defaultdict(int) for e in micro: outcomes[e.get("outcome", "?")] += 1 occupancies = [e["occupancy_pct"] for e in micro if e.get("occupancy_pct") is not None] saved = sum(-(e.get("tokens_delta") or 0) for e in micro) absorbed = [e for e in micro if e.get("outcome") == "absorbed"] durations = [e.get("duration_ms") or 0 for e in micro] if per_session: print(f"{'session':<26} {'passes':>6} {'occupancy%':>18} {'batch':>6} {'saved':>10}") print("-" * 72) batch_by_session: dict[str, int] = defaultdict(int) for b in batch: batch_by_session[b.get("session_id") or "(unknown)"] += 1 for sid, evs in sorted(by_session.items(), key=lambda kv: -len(kv[1])): occ = [e["occupancy_pct"] for e in evs if e.get("occupancy_pct") is not None] spread = pct(occ) occ_s = f"{spread[0]:.0f}-{spread[2]:.0f} (med {spread[1]:.0f})" if spread else "-" s = sum(-(e.get("tokens_delta") or 0) for e in evs) print(f"{sid[:26]:<26} {len(evs):>6} {occ_s:>18} " f"{batch_by_session.get(sid, 0):>6} {s:>+10,}") print() print("-- headroom ----------------------------------") spread = pct(occupancies) if spread: print(f"context occupancy min {spread[0]:.0f}% median {spread[1]:.0f}% max {spread[2]:.0f}%") print(" (% of the batch-compaction threshold)") else: print("context occupancy unavailable (window not resolved when logged)") print(f"batch compactions {len(batch):,}") if batch: print(f" micro passes each {len(micro) / len(batch):.1f}") else: print(" none fired -- the long pause never happened in this log") print() print("-- activity ----------------------------------") print(f"sessions {len(by_session):,}") print(f"passes {len(micro):,}") for name, count in sorted(outcomes.items(), key=lambda kv: -kv[1]): print(f" {name:<20} {count:,}") if durations: ordered = sorted(durations) print(f"pass duration median {ordered[len(ordered) // 2]:,} ms " f"max {ordered[-1]:,} ms") print() print("-- tokens (least interesting) ----------------") print(f"net tokens saved {saved:+,}") if absorbed: sizes = [e.get("exchange_tokens") or 0 for e in absorbed] print(f"exchanges absorbed {len(absorbed):,} " f"(mean {sum(sizes) // len(absorbed):,} tokens each)") print("note: the first pass in a session costs ~400 tokens of marker") print("scaffolding; it pays back from the second pass on.") return 0 def main() -> int: ap = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("logs", nargs="*", type=Path, help="log files (default: agent.log)") ap.add_argument("--per-session", action="store_true", help="break down by session") args = ap.parse_args() paths = args.logs or [default_log()] for p in paths: if not p.exists(): print(f"warning: {p} does not exist", file=sys.stderr) micro, batch = load([p for p in paths if p.exists()]) return report(micro, batch, args.per_session) if __name__ == "__main__": sys.exit(main())