From 038c1ad8724614ea2a76d72c1792ce7b172a708d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:21:04 -0700 Subject: [PATCH] docs(gateway): precise watchdog scope, explicit import-resets-activity contract (review S4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config docs now describe session_stall_timeout precisely: a RECOVERY notifier for an in-process AIAgent with an adapter-queued follow-up — not a general gateway/session stall detector — with a per-AIAgent scan cadence (not globally coordinated per durable session). - import_sessions documents the deliberate export-includes / import-resets asymmetry for the activity fields (no resurrected 'working' labels on machines where no agent runs), with a regression pinning both halves. - Strip trailing whitespace in contributors/emails/fangliquan@qq.com (git diff --check housekeeping). PR #76354 review, scope/contract items + housekeeping. --- contributors/emails/fangliquan@qq.com | 4 +-- hermes_cli/config_defaults.py | 13 ++++++--- hermes_state_portability.py | 9 ++++++ tests/gateway/test_watchdog_review_76354.py | 32 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/contributors/emails/fangliquan@qq.com b/contributors/emails/fangliquan@qq.com index 2d2320ef90bae..b1e421acaef09 100644 --- a/contributors/emails/fangliquan@qq.com +++ b/contributors/emails/fangliquan@qq.com @@ -1,2 +1,2 @@ -fangliquanflq -# PR #73031 author email +fangliquanflq +# PR #73031 author email diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 834263a3c5d2f..255da8e0a1cb4 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -178,10 +178,15 @@ DEFAULT_CONFIG = { # (60+ tool iterations with tiny output) before users assume the # bot is dead and /restart. "gateway_notify_interval": 180, - # Session stall watchdog (seconds): when a gateway session has a - # pending inbound message AND the running agent has not updated its - # activity clock for this long, log a WARNING and notify the user to - # try /new. Distinct from gateway_timeout (which kills the turn) and + # Session stall watchdog (seconds). Scope (#76354): this is a + # RECOVERY notifier for an in-process AIAgent that has an + # adapter-queued follow-up (pending inbound / queued event) while its + # activity clock is stale — NOT a general gateway/session stall + # detector. It does not observe startup restoration, build sentinels, + # turn leases, debounce state, or work owned by another process; the + # scan cadence is per AIAgent instance, not globally coordinated per + # durable session. Notify-only: warns the user to try /new. Distinct + # from gateway_timeout (which kills the turn) and # gateway_notify_interval ("still working" heartbeats). 0 = disable. "session_stall_timeout": 300, # Freshness window for the gateway auto-continue note (seconds). diff --git a/hermes_state_portability.py b/hermes_state_portability.py index dbedb6f29c09d..c25ecb85d6bee 100644 --- a/hermes_state_portability.py +++ b/hermes_state_portability.py @@ -332,6 +332,15 @@ class SessionPortabilityMixin: fail foreign-key validation. Gateway routing, handoff, rewind, and other live runtime state are intentionally reset: this restores conversation history, not ownership of a live channel or process. + + Activity contract (#76354 review S4): export INCLUDES the live + activity fields (``last_activity_at`` / ``last_activity_description`` + / ``last_activity_provenance``) because they are part of the durable + row, but import deliberately RESETS them to NULL. Resurrecting a + stale "working ..." label on a machine where no agent is running + would fabricate activity the watchdog and session listings act on. + This asymmetry is intentional and covered by regression + (tests/gateway/test_watchdog_review_76354.py::test_s4_export_includes_activity_import_resets_it). """ if not isinstance(sessions, list): raise ValueError("sessions must be a list") diff --git a/tests/gateway/test_watchdog_review_76354.py b/tests/gateway/test_watchdog_review_76354.py index 302925cb701f3..94eaaa3c85352 100644 --- a/tests/gateway/test_watchdog_review_76354.py +++ b/tests/gateway/test_watchdog_review_76354.py @@ -225,3 +225,35 @@ async def test_s2_still_stale_after_revalidation_delivers(): sent = await runner._check_session_stalls(60) assert sent == 1 assert adapter.sent and "/new" in adapter.sent[0]["content"] + + +# ── S4: export includes activity fields; import resets them ───────────────── + + +def test_s4_export_includes_activity_import_resets_it(tmp_path): + src = SessionDB(db_path=tmp_path / "src.db") + sid = "S4_PORTABILITY" + src.create_session(sid, source="cli") + src.append_message(sid, "user", "hello") + src.touch_session_activity( + sid, + time.time(), + description="working on something", + provenance=ActivityProvenance.AGENT_COMPRESSION, + ) + + exported = src.export_session(sid) + # Export INCLUDES the live activity fields (part of the durable row). + assert exported["last_activity_at"] is not None + assert exported["last_activity_description"] == "working on something" + + dst = SessionDB(db_path=tmp_path / "dst.db") + result = dst.import_sessions([exported]) + assert sid in result.get("imported_ids", result.get("imported", [sid])) + + row = dst.get_session(sid) + # Import RESETS activity: no resurrected "working" label on a machine + # where no agent is running (explicit contract, #76354 S4). + assert row.get("last_activity_at") is None + assert not row.get("last_activity_description") + assert not row.get("last_activity_provenance")