mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): reap abandoned interactive chats (M3)
An interactive intake/secretary chat the human abandoned (closed the tab without confirming or stopping) leaked its container until the orchestrator restarted — the wedged-grok reaper is task-driven and these run task_id=None, and an SSE disconnect intentionally does NOT reap (so a page reload can reconnect). Reap by IDLE TIME, not connection state: PrompterLiveRegistry tracks last_activity (bumped on every push/deliver = a turn), and the 60s sweeper retires sessions idle past ROBOCO_INTERACTIVE_IDLE_REAP_SECONDS (default 1800; 0 disables) via reap_intake_session / reap_secretary_session. An active or page-reloaded chat that keeps exchanging turns stays fresh and is never reaped; board-review-parked sessions (task_id set) are exempt. Provider-agnostic — fixes the leak for both Claude and Grok interactive. Tests: idle-only reap (active/parked/closed excluded), activity bump keeps a session alive, threshold 0 disables. Gate green (ruff/mypy/xenon + prompter_live).
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -22,6 +23,35 @@ def test_open_get_close() -> None:
|
||||
assert reg.get("s1") is None
|
||||
|
||||
|
||||
def test_idle_session_ids_reaps_only_abandoned_chats() -> None:
|
||||
reg = PrompterLiveRegistry()
|
||||
old = reg.open("idle", "intake-1")
|
||||
old.last_activity = time.monotonic() - 4000 # silent for >1h
|
||||
reg.open("fresh", "intake-1") # just opened — active
|
||||
parked = reg.open("parked", "intake-1") # board-review parked
|
||||
parked.last_activity = time.monotonic() - 4000
|
||||
parked.task_id = "task-9"
|
||||
reg.open("done", "secretary-1")
|
||||
reg.close("done") # closed sessions excluded
|
||||
|
||||
idle = dict(reg.idle_session_ids(1800))
|
||||
assert "idle" in idle and idle["idle"] == "intake-1" # abandoned -> reaped
|
||||
assert "fresh" not in idle # active -> kept
|
||||
assert "parked" not in idle # board-review parked -> exempt
|
||||
assert "done" not in idle # closed -> excluded
|
||||
# Disabled when threshold <= 0.
|
||||
assert reg.idle_session_ids(0) == []
|
||||
|
||||
|
||||
def test_activity_bump_keeps_session_alive() -> None:
|
||||
reg = PrompterLiveRegistry()
|
||||
s = reg.open("s1", "intake-1")
|
||||
s.last_activity = time.monotonic() - 4000
|
||||
assert ("s1", "intake-1") in reg.idle_session_ids(1800)
|
||||
reg.push("s1", {"kind": "text", "text": "hi"}) # an agent turn = activity
|
||||
assert reg.idle_session_ids(1800) == [] # no longer idle
|
||||
|
||||
|
||||
def test_close_by_agent_closes_matching_sessions_with_error() -> None:
|
||||
reg = PrompterLiveRegistry()
|
||||
reg.open("s1", "intake-1")
|
||||
|
||||
Reference in New Issue
Block a user