docs(gateway): precise watchdog scope, explicit import-resets-activity contract (review S4)

- 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.
This commit is contained in:
Teknium 2026-08-01 15:21:04 -07:00
parent 89c4e26e23
commit 038c1ad872
4 changed files with 52 additions and 6 deletions

View File

@ -1,2 +1,2 @@
fangliquanflq
# PR #73031 author email
fangliquanflq
# PR #73031 author email

View File

@ -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).

View File

@ -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")

View File

@ -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")