Merge pull request #2138 into stage-344

fix: recover from stale deleted workspaces
This commit is contained in:
Hermes Agent
2026-05-12 16:12:58 +00:00
3 changed files with 97 additions and 3 deletions
+19 -1
View File
@@ -6967,7 +6967,7 @@ def _handle_chat_start(handler, body, diag=None):
attachments = _normalize_chat_attachments(body.get("attachments") or [])[:20]
diag.stage("resolve_workspace") if diag else None
try:
workspace = str(resolve_trusted_workspace(body.get("workspace") or s.workspace))
workspace = _resolve_chat_workspace_with_recovery(s, body.get("workspace"))
except ValueError as e:
return bad(handler, str(e))
requested_model = body.get("model") or s.model
@@ -7000,6 +7000,24 @@ def _handle_chat_start(handler, body, diag=None):
def _resolve_chat_workspace_with_recovery(s, requested_workspace) -> str:
"""Recover stale implicit session workspaces without hiding explicit errors."""
explicit = requested_workspace not in (None, "")
candidate = requested_workspace if explicit else getattr(s, "workspace", None)
try:
return str(resolve_trusted_workspace(candidate))
except ValueError:
if explicit:
raise
fallback = str(resolve_trusted_workspace(get_last_workspace()))
s.workspace = fallback
try:
s.save()
except Exception:
pass
return fallback
def _normalize_chat_attachments(raw_attachments):
"""Normalize attachment payloads from the browser.
+7 -2
View File
@@ -64,7 +64,7 @@ def _profile_default_workspace() -> str:
2. 'default_workspace' — alternate explicit key
3. 'terminal.cwd' — hermes-agent terminal working dir (most common)
Falls back to the boot-time DEFAULT_WORKSPACE constant.
Falls back to the live DEFAULT_WORKSPACE from api.config.
"""
try:
from api.config import get_config
@@ -86,7 +86,12 @@ def _profile_default_workspace() -> str:
return str(p)
except (ImportError, Exception):
logger.debug("Failed to load profile default workspace config")
return str(_BOOT_DEFAULT_WORKSPACE)
try:
from api.config import DEFAULT_WORKSPACE as _LIVE_DEFAULT_WORKSPACE
return str(Path(_LIVE_DEFAULT_WORKSPACE).expanduser().resolve())
except Exception:
return str(Path(_BOOT_DEFAULT_WORKSPACE).expanduser().resolve())
# ── Public API ──────────────────────────────────────────────────────────────
+71
View File
@@ -0,0 +1,71 @@
from pathlib import Path
from types import SimpleNamespace
import pytest
from api import config as api_config
from api import routes, workspace
def test_profile_default_workspace_uses_live_config_default(monkeypatch, tmp_path):
live_default = tmp_path / "live-default"
live_default.mkdir()
monkeypatch.setattr(api_config, "DEFAULT_WORKSPACE", live_default)
monkeypatch.setattr(api_config, "get_config", lambda: {})
assert workspace._profile_default_workspace() == str(live_default.resolve())
def test_resolve_chat_workspace_with_recovery_repairs_missing_implicit_workspace(monkeypatch, tmp_path):
fallback = tmp_path / "fallback"
fallback.mkdir()
stale = tmp_path / "deleted-workspace"
def fake_resolve(value):
if value == str(stale):
raise ValueError(f"Path does not exist: {stale}")
return Path(value).resolve()
saved = {"count": 0}
def fake_save():
saved["count"] += 1
session = SimpleNamespace(session_id="sess-1", workspace=str(stale), save=fake_save)
monkeypatch.setattr(routes, "resolve_trusted_workspace", fake_resolve)
monkeypatch.setattr(routes, "get_last_workspace", lambda: str(fallback))
resolved = routes._resolve_chat_workspace_with_recovery(session, None)
assert resolved == str(fallback.resolve())
assert session.workspace == str(fallback.resolve())
assert saved["count"] == 1
def test_resolve_chat_workspace_with_recovery_preserves_explicit_errors(monkeypatch, tmp_path):
fallback = tmp_path / "fallback"
fallback.mkdir()
stale = tmp_path / "deleted-workspace"
def fake_resolve(value):
if value == str(stale):
raise ValueError(f"Path does not exist: {stale}")
return Path(value).resolve()
saved = {"count": 0}
def fake_save():
saved["count"] += 1
session = SimpleNamespace(session_id="sess-2", workspace=str(fallback), save=fake_save)
monkeypatch.setattr(routes, "resolve_trusted_workspace", fake_resolve)
monkeypatch.setattr(routes, "get_last_workspace", lambda: str(fallback))
with pytest.raises(ValueError, match="Path does not exist"):
routes._resolve_chat_workspace_with_recovery(session, str(stale))
assert session.workspace == str(fallback)
assert saved["count"] == 0