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:
Gille
2026-06-19 16:06:38 -06:00
committed by GitHub
parent 1a0ef1311c
commit a7983d5ad7
8 changed files with 129 additions and 11 deletions
+5
View File
@@ -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
View File
@@ -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),
+15
View File
@@ -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)
+35
View File
@@ -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)
+31
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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 } : {}),
});
})