hermes-agent/tests/agent/test_session_activity.py
fangliquanflq cfb206fe2e feat(gateway): session activity watchdog, stall notify, compress timeout (#72424)
Three mechanisms to detect and notify when gateway sessions stall silently:

1. Mid-turn activity heartbeats stamped to SessionDB so hermes sessions list
   and hermes status show progress during long turns without new message rows.

2. Stall watchdog: when a busy session has pending inbound and the shared
   activity clock is idle past agent.session_stall_timeout (default 300),
   log a WARNING and notify the user once to try /new. Notify-only; does
   not kill the turn.

3. Compaction timeout: fenceless compress_context callers get a progress-aware
   host budget (compression.context_timeout_seconds default 120 idle,
   compression.context_total_ceiling_seconds default 600 ceiling). On timeout,
   cancel via commit fence, skip compaction without dropping messages, and
   continue the turn.

Closes #72016 (slices 1-3; slice 4 cumulative SSE stream-retry deadline
remains a follow-up).

Cherry-picked from PR #72424 by @fangliquanflq.
2026-07-28 00:44:02 +05:30

82 lines
3.1 KiB
Python

"""Unit tests for the shared session activity observation contract."""
from agent.session_activity import (
ActivityProvenance,
bound_activity_description,
build_activity_snapshot,
normalize_activity_provenance,
)
def test_bound_activity_description_truncates():
long = "x" * 200
out = bound_activity_description(long)
assert len(out) == 120
assert out.endswith("")
def test_normalize_activity_provenance_defaults_to_unknown():
assert normalize_activity_provenance(None) is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("") is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("not-a-real-source") is ActivityProvenance.UNKNOWN
assert normalize_activity_provenance("agent.activity") is ActivityProvenance.UNKNOWN
assert (
normalize_activity_provenance(ActivityProvenance.AGENT_COMPRESSION)
is ActivityProvenance.AGENT_COMPRESSION
)
assert (
normalize_activity_provenance("agent.compression_timeout")
is ActivityProvenance.AGENT_COMPRESSION_TIMEOUT
)
def test_build_activity_snapshot_includes_compat_aliases():
snap = build_activity_snapshot(
last_activity_at=100.0,
last_activity_description="starting API call #1",
last_activity_provenance=ActivityProvenance.UNKNOWN,
now=110.0,
extra={"api_call_count": 1},
)
assert snap["last_activity_at"] == 100.0
assert snap["last_activity_description"] == "starting API call #1"
assert snap["last_activity_provenance"] == "unknown"
assert snap["seconds_since_activity"] == 10.0
assert snap["last_activity_ts"] == 100.0
assert snap["last_activity_desc"] == "starting API call #1"
assert snap["description"] == "starting API call #1"
assert snap["api_call_count"] == 1
assert "phase" not in snap
assert "last_progress_at" not in snap
def test_build_activity_snapshot_maps_missing_provenance_to_unknown():
snap = build_activity_snapshot(
last_activity_at=1.0,
last_activity_description="starting new turn (cached)",
last_activity_provenance=None,
now=2.0,
)
assert snap["last_activity_provenance"] == "unknown"
def test_build_activity_snapshot_preserves_compression_transition_provenances():
"""Compaction / timeout / cooldown share the observation source (#72424)."""
for provenance, desc in (
(ActivityProvenance.AGENT_COMPRESSION, "context compression in progress"),
(ActivityProvenance.AGENT_COMPRESSION_TIMEOUT, "context compression timed out"),
(
ActivityProvenance.AGENT_COMPRESSION_COOLDOWN,
"compression blocked (cooldown: 30s remaining)",
),
):
snap = build_activity_snapshot(
last_activity_at=50.0,
last_activity_description=desc,
last_activity_provenance=provenance,
now=55.0,
)
assert snap["last_activity_provenance"] == provenance.value
assert snap["provenance"] == provenance.value
assert snap["last_activity_description"] == desc
assert snap["seconds_since_activity"] == 5.0