fix(dashboard): hide sidecar sessions from history (#49269)
* fix(dashboard): hide sidecar sessions from history * test(dashboard): allow sidecar source in session payload
This commit is contained in:
@@ -49,6 +49,7 @@ _UNSET: Any = object()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SESSION_PLATFORM: ContextVar = ContextVar("HERMES_SESSION_PLATFORM", default=_UNSET)
|
||||
_SESSION_SOURCE: ContextVar = ContextVar("HERMES_SESSION_SOURCE", default=_UNSET)
|
||||
_SESSION_CHAT_ID: ContextVar = ContextVar("HERMES_SESSION_CHAT_ID", default=_UNSET)
|
||||
_SESSION_CHAT_NAME: ContextVar = ContextVar("HERMES_SESSION_CHAT_NAME", default=_UNSET)
|
||||
_SESSION_THREAD_ID: ContextVar = ContextVar("HERMES_SESSION_THREAD_ID", default=_UNSET)
|
||||
@@ -69,6 +70,7 @@ _CRON_AUTO_DELIVER_THREAD_ID: ContextVar = ContextVar("HERMES_CRON_AUTO_DELIVER_
|
||||
|
||||
_VAR_MAP = {
|
||||
"HERMES_SESSION_PLATFORM": _SESSION_PLATFORM,
|
||||
"HERMES_SESSION_SOURCE": _SESSION_SOURCE,
|
||||
"HERMES_SESSION_CHAT_ID": _SESSION_CHAT_ID,
|
||||
"HERMES_SESSION_CHAT_NAME": _SESSION_CHAT_NAME,
|
||||
"HERMES_SESSION_THREAD_ID": _SESSION_THREAD_ID,
|
||||
@@ -100,6 +102,7 @@ def set_current_session_id(session_id: str) -> None:
|
||||
|
||||
def set_session_vars(
|
||||
platform: str = "",
|
||||
source: str = "",
|
||||
chat_id: str = "",
|
||||
chat_name: str = "",
|
||||
thread_id: str = "",
|
||||
@@ -122,6 +125,7 @@ def set_session_vars(
|
||||
"""
|
||||
tokens = [
|
||||
_SESSION_PLATFORM.set(platform),
|
||||
_SESSION_SOURCE.set(source),
|
||||
_SESSION_CHAT_ID.set(chat_id),
|
||||
_SESSION_CHAT_NAME.set(chat_name),
|
||||
_SESSION_THREAD_ID.set(thread_id),
|
||||
@@ -153,6 +157,7 @@ def clear_session_vars(tokens: list) -> None:
|
||||
"""
|
||||
for var in (
|
||||
_SESSION_PLATFORM,
|
||||
_SESSION_SOURCE,
|
||||
_SESSION_CHAT_ID,
|
||||
_SESSION_CHAT_NAME,
|
||||
_SESSION_THREAD_ID,
|
||||
|
||||
+15
-2
@@ -89,6 +89,19 @@ def _launch_cwd_for_session(source: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _session_source_for_agent(platform: Optional[str]) -> str:
|
||||
try:
|
||||
from gateway.session_context import get_session_env
|
||||
|
||||
source = get_session_env("HERMES_SESSION_SOURCE", "")
|
||||
except Exception:
|
||||
source = os.environ.get("HERMES_SESSION_SOURCE", "")
|
||||
source = str(source or "").strip()
|
||||
if source:
|
||||
return source
|
||||
return platform or "cli"
|
||||
|
||||
|
||||
# OpenAI lazy proxy + safe stdio + proxy URL helpers — see agent/process_bootstrap.py.
|
||||
# `OpenAI` is re-exported here so `patch("run_agent.OpenAI", ...)` in tests works.
|
||||
# The other `# noqa: F401` re-exports below cover names accessed via
|
||||
@@ -512,7 +525,7 @@ class AIAgent:
|
||||
"""Create session DB row on first use. Disables _session_db on failure."""
|
||||
if self._session_db_created or not self._session_db:
|
||||
return
|
||||
source = self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli")
|
||||
source = _session_source_for_agent(self.platform)
|
||||
try:
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
@@ -578,7 +591,7 @@ class AIAgent:
|
||||
start_context = {
|
||||
"old_session_id": old_session_id,
|
||||
"carry_over_context": carry_over_context,
|
||||
"platform": getattr(self, "platform", None) or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
"platform": _session_source_for_agent(getattr(self, "platform", None)),
|
||||
"model": getattr(self, "model", ""),
|
||||
"context_length": getattr(engine, "context_length", None),
|
||||
"conversation_id": getattr(self, "_gateway_session_key", None),
|
||||
|
||||
@@ -45,6 +45,7 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
context = SessionContext(source=source, connected_platforms=[], home_channels={})
|
||||
|
||||
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_ID", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_NAME", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_USER_ID", raising=False)
|
||||
@@ -55,6 +56,7 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
|
||||
# Values should be readable via get_session_env (contextvar path)
|
||||
assert get_session_env("HERMES_SESSION_PLATFORM") == "telegram"
|
||||
assert get_session_env("HERMES_SESSION_SOURCE") == ""
|
||||
assert get_session_env("HERMES_SESSION_CHAT_ID") == "-1001"
|
||||
assert get_session_env("HERMES_SESSION_CHAT_NAME") == "Group"
|
||||
assert get_session_env("HERMES_SESSION_USER_ID") == "123456"
|
||||
@@ -63,12 +65,25 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
|
||||
# os.environ should NOT be touched
|
||||
assert os.getenv("HERMES_SESSION_PLATFORM") is None
|
||||
assert os.getenv("HERMES_SESSION_SOURCE") is None
|
||||
assert os.getenv("HERMES_SESSION_THREAD_ID") is None
|
||||
|
||||
# Clean up
|
||||
runner._clear_session_env(tokens)
|
||||
|
||||
|
||||
def test_session_source_uses_contextvars(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
|
||||
|
||||
tokens = set_session_vars(source="tool")
|
||||
|
||||
assert get_session_env("HERMES_SESSION_SOURCE") == "tool"
|
||||
|
||||
clear_session_vars(tokens)
|
||||
|
||||
assert get_session_env("HERMES_SESSION_SOURCE") == ""
|
||||
|
||||
|
||||
def test_clear_session_env_restores_previous_state(monkeypatch):
|
||||
"""_clear_session_env should restore contextvars to their pre-handler values."""
|
||||
runner = object.__new__(GatewayRunner)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
|
||||
from gateway.session_context import _UNSET, _VAR_MAP, clear_session_vars, set_session_vars
|
||||
from run_agent import _session_source_for_agent
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_contextvars():
|
||||
for var in _VAR_MAP.values():
|
||||
var.set(_UNSET)
|
||||
yield
|
||||
for var in _VAR_MAP.values():
|
||||
var.set(_UNSET)
|
||||
|
||||
|
||||
def test_session_source_context_overrides_platform(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
|
||||
|
||||
tokens = set_session_vars(source="tool")
|
||||
try:
|
||||
assert _session_source_for_agent("tui") == "tool"
|
||||
finally:
|
||||
clear_session_vars(tokens)
|
||||
|
||||
|
||||
def test_session_source_falls_back_to_platform(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
|
||||
|
||||
assert _session_source_for_agent("tui") == "tui"
|
||||
|
||||
|
||||
def test_session_source_falls_back_to_env(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_SESSION_SOURCE", "webhook")
|
||||
|
||||
assert _session_source_for_agent(None) == "webhook"
|
||||
@@ -17,9 +17,9 @@ def test_sidecar_session_create_scopes_profile():
|
||||
"""The sidecar must pass the dashboard's selected profile so model/credential
|
||||
info matches the PTY child under profile-scoped chat."""
|
||||
source = CHAT_SIDEBAR.read_text(encoding="utf-8")
|
||||
assert '"session.create"' in source
|
||||
assert re.search(
|
||||
r"close_on_disconnect:\s*true,\s*\.\.\.\(profile\s*\?\s*\{\s*profile\s*\}\s*:\s*\{\}\)",
|
||||
source,
|
||||
re.DOTALL,
|
||||
)
|
||||
call = re.search(r'"session\.create",\s*\{(.*?)\}\);', source, re.DOTALL)
|
||||
assert call, "sidecar session.create call not found"
|
||||
body = call.group(1)
|
||||
assert re.search(r"close_on_disconnect:\s*true", body)
|
||||
assert re.search(r'source:\s*"tool"', body)
|
||||
assert re.search(r"\.\.\.\(profile\s*\?\s*\{\s*profile\s*\}\s*:\s*\{\}\)", body)
|
||||
|
||||
@@ -2016,6 +2016,25 @@ def test_ensure_session_db_row_persists_explicit_cwd(monkeypatch, tmp_path):
|
||||
]
|
||||
|
||||
|
||||
def test_ensure_session_db_row_persists_session_source(monkeypatch):
|
||||
created = []
|
||||
|
||||
class _FakeDB:
|
||||
def create_session(self, key, source=None, model=None, model_config=None, cwd=None):
|
||||
created.append(
|
||||
{"key": key, "source": source, "model": model, "model_config": model_config, "cwd": cwd}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||
monkeypatch.setattr(server, "_resolve_model", lambda: "test-model")
|
||||
|
||||
server._ensure_session_db_row({"session_key": "k1", "source": "tool"})
|
||||
|
||||
assert created == [
|
||||
{"key": "k1", "source": "tool", "model": "test-model", "model_config": None, "cwd": None}
|
||||
]
|
||||
|
||||
|
||||
def test_ensure_session_db_row_defaults_to_no_workspace(monkeypatch, tmp_path):
|
||||
"""Without an explicit workspace, cwd is left null so the session groups
|
||||
under "No workspace" rather than the gateway's launch directory."""
|
||||
@@ -7686,6 +7705,18 @@ def test_session_create_records_close_on_disconnect_flag(monkeypatch):
|
||||
server._sessions.clear()
|
||||
|
||||
|
||||
def test_session_create_records_source(monkeypatch):
|
||||
monkeypatch.setattr(server, "_start_agent_build", lambda sid, session: None)
|
||||
server._sessions.clear()
|
||||
try:
|
||||
sid = server.handle_request(
|
||||
{"id": "1", "method": "session.create", "params": {"source": "tool"}}
|
||||
)["result"]["session_id"]
|
||||
assert server._sessions[sid]["source"] == "tool"
|
||||
finally:
|
||||
server._sessions.clear()
|
||||
|
||||
|
||||
def test_shutdown_sessions_closes_every_session_via_helper(monkeypatch):
|
||||
seen = []
|
||||
monkeypatch.setattr(
|
||||
|
||||
+21
-3
@@ -1148,6 +1148,14 @@ def _session_cwd(session: dict | None) -> str:
|
||||
return _completion_cwd()
|
||||
|
||||
|
||||
def _session_source(session: dict | None) -> str:
|
||||
if session:
|
||||
source = str(session.get("source") or "").strip()
|
||||
if source:
|
||||
return source
|
||||
return "tui"
|
||||
|
||||
|
||||
def _register_session_cwd(session: dict | None) -> None:
|
||||
if not session:
|
||||
return
|
||||
@@ -1247,7 +1255,7 @@ def _ensure_session_db_row(session: dict) -> None:
|
||||
try:
|
||||
db.create_session(
|
||||
key,
|
||||
source="tui",
|
||||
source=_session_source(session),
|
||||
model=row_model,
|
||||
model_config=model_config or None,
|
||||
cwd=_session_cwd(session) if session.get("explicit_cwd") else None,
|
||||
@@ -1416,7 +1424,13 @@ def _set_session_context(session_key: str, cwd: str | None = None) -> list:
|
||||
# know the parent workspace pass it explicitly so spawned agents inherit
|
||||
# it instead of falling back to the gateway launch dir.
|
||||
resolved = cwd if cwd is not None else _cwd_for_session_key(session_key)
|
||||
return set_session_vars(session_key=session_key, cwd=resolved)
|
||||
source = "tui"
|
||||
with _sessions_lock:
|
||||
for sess in list(_sessions.values()):
|
||||
if sess.get("session_key") == session_key:
|
||||
source = _session_source(sess)
|
||||
break
|
||||
return set_session_vars(session_key=session_key, source=source, cwd=resolved)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@@ -4183,6 +4197,7 @@ def _(rid, params: dict) -> dict:
|
||||
except Exception:
|
||||
explicit_cwd = False
|
||||
resolved_cwd = _completion_cwd(params)
|
||||
source = str(params.get("source") or "tui").strip() or "tui"
|
||||
_enable_gateway_prompts()
|
||||
|
||||
# ``profile`` (app-global remote mode): a new chat started under a non-launch
|
||||
@@ -4248,6 +4263,7 @@ def _(rid, params: dict) -> dict:
|
||||
"running": False,
|
||||
"session_key": key,
|
||||
"show_reasoning": _load_show_reasoning(),
|
||||
"source": source,
|
||||
"slash_worker": None,
|
||||
"tool_progress_mode": _load_tool_progress_mode(),
|
||||
"tool_started_at": {},
|
||||
@@ -4521,6 +4537,7 @@ def _(rid, params: dict) -> dict:
|
||||
# report its liveness from the relay registry so the window paints a
|
||||
# busy indicator instead of a dead idle transcript.
|
||||
child_running = _child_run_active(target)
|
||||
source = str(params.get("source") or "tui").strip() or "tui"
|
||||
with _session_resume_lock:
|
||||
live = _find_live_session_by_key(target)
|
||||
if live is not None:
|
||||
@@ -4556,6 +4573,7 @@ def _(rid, params: dict) -> dict:
|
||||
"running": False,
|
||||
"session_key": target,
|
||||
"show_reasoning": _load_show_reasoning(),
|
||||
"source": source,
|
||||
"slash_worker": None,
|
||||
"tool_progress_mode": _load_tool_progress_mode(),
|
||||
"tool_started_at": {},
|
||||
@@ -5753,7 +5771,7 @@ def _(rid, params: dict) -> dict:
|
||||
)
|
||||
db.create_session(
|
||||
new_key,
|
||||
source="tui",
|
||||
source=_session_source(session),
|
||||
model=_resolve_model(),
|
||||
# Stable _branched_from marker so list_sessions_rich() keeps the
|
||||
# branch visible in /resume and /sessions. The TUI branch leaves
|
||||
|
||||
@@ -199,6 +199,7 @@ export function ChatSidebar({
|
||||
// slash_worker subprocess) when the WS drops, instead of leaking it.
|
||||
return gw.request<{ session_id: string }>("session.create", {
|
||||
close_on_disconnect: true,
|
||||
source: "tool",
|
||||
...(profile ? { profile } : {}),
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user