fix(prompt): preserve resumed workspace provenance

This commit is contained in:
fangliquanflq
2026-08-29 05:56:22 +08:00
committed by Teknium
parent c6ee4e0809
commit da090aa4ba
5 changed files with 114 additions and 2 deletions
+46
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import contextlib
import os
import subprocess
import threading
from pathlib import Path
import pytest
@@ -511,6 +512,51 @@ def test_explicit_desktop_and_terminal_cwds_are_context_workspaces():
) is False
@pytest.mark.parametrize(
("explicit_cwd", "launch_artifact"),
[(True, False), (False, True)],
)
def test_desktop_agent_rebuild_preserves_workspace_provenance(
monkeypatch, explicit_cwd, launch_artifact
):
captured = {}
session = {
"agent": object(),
"attached_images": [],
"cwd": "/picked/repo" if explicit_cwd else "/opt/hermes",
"edit_snapshots": {},
"explicit_cwd": explicit_cwd,
"history": ["old"],
"history_lock": threading.Lock(),
"history_version": 0,
"image_counter": 0,
"running": False,
"session_key": "stored-session",
"show_reasoning": False,
"source": "desktop",
"tool_progress_mode": "off",
"tool_started_at": {},
}
def _make_agent(*_args, **kwargs):
captured.update(kwargs)
return object()
monkeypatch.setattr(server, "_set_session_context", lambda _key: [])
monkeypatch.setattr(server, "_clear_session_context", lambda _tokens: None)
monkeypatch.setattr(server, "_make_agent", _make_agent)
monkeypatch.setattr(server, "_config_model_target", lambda: None)
monkeypatch.setattr(server, "_load_show_reasoning", lambda: False)
monkeypatch.setattr(server, "_load_tool_progress_mode", lambda: "off")
monkeypatch.setattr(server, "_session_info", lambda *_args: {})
monkeypatch.setattr(server, "_emit", lambda *_args: None)
monkeypatch.setattr(server, "_restart_slash_worker", lambda *_args: None)
server._reset_session_agent("live-session", session)
assert captured["context_cwd_is_launch_artifact"] is launch_artifact
def test_home_container_dirs_are_never_a_workspace(tmp_path):
"""`/home` and `/Users` hold homes; they are not workspaces themselves.
@@ -368,6 +368,34 @@ def test_deferred_build_transfers_the_handle_on_success(
assert session["agent"]._owns_session_db is True
def test_deferred_build_uses_the_preserved_desktop_workspace_provenance(
build_env, registered, monkeypatch
):
captured: dict = {}
def _fake_make_agent(*_args, session_db=None, **kwargs):
captured.update(kwargs)
return types.SimpleNamespace(_session_db=session_db, _owns_session_db=False)
monkeypatch.setattr(server, "_make_agent", _fake_make_agent)
monkeypatch.setattr(
server, "_session_source", lambda current: current.get("source")
)
sid, session = "sid-workspace", _session(build_env.profile_home)
session.update(
{
"cwd": "C:/picked/repo",
"explicit_cwd": True,
"source": "desktop",
}
)
registered(sid, session)
_run_build(sid, session)
assert captured["context_cwd_is_launch_artifact"] is False
def test_deferred_build_closes_the_handle_when_the_session_is_reaped_midbuild(
build_env, registered, monkeypatch
):
@@ -134,6 +134,28 @@ def test_resume_closes_profile_db_when_session_not_found(profile_dbs):
assert scoped[0].closed == 1
def test_deferred_desktop_resume_keeps_stored_workspace_provenance(
profile_dbs, monkeypatch, tmp_path
):
workspace = tmp_path / "workspace"
workspace.mkdir()
def _factory(db_path=None, **kwargs):
db = _RecordingDB(db_path=db_path, **kwargs)
db.rows["s1"] = {"id": "s1", "cwd": str(workspace)}
profile_dbs.append(db)
return db
monkeypatch.setattr("hermes_state.SessionDB", _factory)
resp = _resume(session_id="s1", profile="work", source="desktop")
session = server._sessions[resp["result"]["session_id"]]
assert session["cwd"] == str(workspace)
assert session["explicit_cwd"] is True
assert server._context_cwd_is_launch_artifact(session) is False
def test_resume_closes_profile_db_when_reopen_fails(profile_dbs, monkeypatch):
"""The 'resume failed' early return must not leak the handle."""
+5
View File
@@ -689,6 +689,7 @@ def _(rid, params: dict) -> dict:
profile_home=profile_home,
lazy=True,
todo_state=_todo_state_from_history(history),
explicit_cwd=bool(profile_resume_cwd),
)
if (live := _claim_or_reuse_live(sid, target, record, lease)) is not None:
return _reuse_live_response(*live)
@@ -760,6 +761,7 @@ def _(rid, params: dict) -> dict:
profile_home=profile_home,
model_override=overrides.get("model_override"),
resume_runtime_overrides=overrides or None,
explicit_cwd=bool(profile_resume_cwd),
)
record["resume_history_ready"] = threading.Event()
record["resume_hydrating"] = True
@@ -860,6 +862,7 @@ def _(rid, params: dict) -> dict:
model_override=overrides.get("model_override"),
resume_runtime_overrides=overrides or None,
todo_state=_todo_state_from_history(history),
explicit_cwd=bool(profile_resume_cwd),
)
if (live := _claim_or_reuse_live(sid, target, record, lease)) is not None:
return _reuse_live_response(*live)
@@ -998,6 +1001,7 @@ def _(rid, params: dict) -> dict:
cwd=profile_resume_cwd,
session_db=db,
source=source,
explicit_cwd=bool(profile_resume_cwd),
)
# Ownership TRANSFER — the registered session's agent now
# holds this handle for its whole life, and _init_session
@@ -3324,6 +3328,7 @@ def _(rid, params: dict) -> dict:
session_db=branch_db,
source=source,
profile_home=parent_home,
explicit_cwd=bool(session.get("explicit_cwd")),
)
# Ownership TRANSFER — the branched session's agent holds this
# handle for its whole life and closes it on teardown. Drop is
+13 -2
View File
@@ -3221,7 +3221,12 @@ def _start_agent_build(sid: str, session: dict) -> None:
# Lazy-resumed (watch) sessions carry the stored conversation
# id — pass it through so the upgrade continues that session
# instead of starting a fresh one under the same key.
kw = {"session_db": session_db}
kw = {
"session_db": session_db,
"context_cwd_is_launch_artifact": (
_context_cwd_is_launch_artifact(current)
),
}
if resume_sid := current.get("resume_session_id"):
kw["session_id"] = resume_sid
kw["platform_override"] = _session_source(current)
@@ -8672,6 +8677,9 @@ def _reset_session_agent(sid: str, session: dict) -> dict:
session["session_key"],
session_id=session["session_key"],
platform_override=_session_source(session),
context_cwd_is_launch_artifact=(
_context_cwd_is_launch_artifact(session)
),
)
finally:
_clear_session_context(tokens)
@@ -9041,6 +9049,7 @@ def _init_session(
session_db=None,
source: str | None = None,
profile_home: str | None = None,
explicit_cwd: bool = False,
):
now = time.time()
with _sessions_lock:
@@ -9057,6 +9066,7 @@ def _init_session(
"attached_images": [],
"image_counter": 0,
"cwd": cwd or _completion_cwd(),
"explicit_cwd": bool(explicit_cwd),
"cols": cols,
"slash_worker": None,
"show_reasoning": _load_show_reasoning(),
@@ -10462,6 +10472,7 @@ def _deferred_session_record(
model_override=None,
resume_runtime_overrides: dict | None = None,
todo_state: dict | None = None,
explicit_cwd: bool = False,
) -> dict:
"""A live-session record whose AIAgent is built later (lazy watch / cold
resume) — _init_session's shape minus the agent."""
@@ -10478,7 +10489,7 @@ def _deferred_session_record(
"cwd": cwd,
"display_history_prefix": display_history_prefix or [],
"edit_snapshots": {},
"explicit_cwd": False,
"explicit_cwd": bool(explicit_cwd),
"history": history,
"history_lock": threading.Lock(),
"history_version": 0,