fix(gateway): make bg-process reset TTL configurable + surface session-scoped processes

Follow-up to the cherry-picked #29212 (#29177):

- Promote the 24h stale-process threshold to config.yaml
  (session_reset.bg_process_max_age_hours) instead of a hardcoded
  constant. 0 disables the cutoff (legacy: any live process blocks reset).
  Wired through GatewayConfig.default_reset_policy in gateway/run.py.
- Bug 2: process(action=list) now resolves the gateway session_key from
  the contextvar and surfaces session-scoped background processes (a
  forgotten preview server under a different task), flagged
  session_scoped — so the agent/user can discover and kill the blocker.
  Previously the task-scoped list returned [] and the blocker was invisible.
- Tests: config round-trip for the new field, cross-task list visibility.
- Docs: messaging session-reset section.
This commit is contained in:
teknium1 2026-06-27 19:53:29 -07:00 committed by Teknium
parent 33d8b66d5b
commit a1ac6baac4
6 changed files with 101 additions and 13 deletions

View file

@ -138,26 +138,31 @@ class TestGetConnectedPlatforms:
class TestSessionResetPolicy:
def test_roundtrip(self):
policy = SessionResetPolicy(mode="idle", at_hour=6, idle_minutes=120)
policy = SessionResetPolicy(mode="idle", at_hour=6, idle_minutes=120,
bg_process_max_age_hours=48)
d = policy.to_dict()
restored = SessionResetPolicy.from_dict(d)
assert restored.mode == "idle"
assert restored.at_hour == 6
assert restored.idle_minutes == 120
assert restored.bg_process_max_age_hours == 48
def test_defaults(self):
policy = SessionResetPolicy()
assert policy.mode == "both"
assert policy.at_hour == 4
assert policy.idle_minutes == 1440
assert policy.bg_process_max_age_hours == 24
def test_from_dict_treats_null_values_as_defaults(self):
restored = SessionResetPolicy.from_dict(
{"mode": None, "at_hour": None, "idle_minutes": None}
{"mode": None, "at_hour": None, "idle_minutes": None,
"bg_process_max_age_hours": None}
)
assert restored.mode == "both"
assert restored.at_hour == 4
assert restored.idle_minutes == 1440
assert restored.bg_process_max_age_hours == 24
def test_from_dict_coerces_quoted_false_notify(self):
restored = SessionResetPolicy.from_dict({"notify": "false"})

View file

@ -416,6 +416,34 @@ class TestListSessions:
assert len(result) == 1
assert result[0]["session_id"] == "proc_1"
def test_session_key_surfaces_cross_task_processes(self, registry):
"""A bg process under the same gateway session but a DIFFERENT task is
surfaced when session_key is passed, and flagged session_scoped (#29177).
"""
# Current turn's task = "t_now"; forgotten preview server = "t_old"
# but both share gateway session_key "gw1".
own = _make_session(sid="proc_own", task_id="t_now")
own.session_key = "gw1"
forgotten = _make_session(sid="proc_forgotten", task_id="t_old")
forgotten.session_key = "gw1"
other = _make_session(sid="proc_other", task_id="t_x")
other.session_key = "gw_other"
registry._running[own.id] = own
registry._running[forgotten.id] = forgotten
registry._running[other.id] = other
# Task-only (legacy) view sees just the current task's process.
legacy = registry.list_sessions(task_id="t_now")
assert {r["session_id"] for r in legacy} == {"proc_own"}
# With session_key, the forgotten process under the same gateway
# session is surfaced and flagged; the unrelated session is not.
result = registry.list_sessions(task_id="t_now", session_key="gw1")
by_id = {r["session_id"]: r for r in result}
assert set(by_id) == {"proc_own", "proc_forgotten"}
assert by_id["proc_forgotten"].get("session_scoped") is True
assert "session_scoped" not in by_id["proc_own"]
def test_list_entry_fields(self, registry):
s = _make_session(output="preview text")
registry._running[s.id] = s