Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions coding-agent/src/coding_agent/ui/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
logger = logging.getLogger(__name__)

_CHECKPOINT_SESSION_CONFIG_KEY = "session_restart_config"
_DEFAULT_EXECUTION_BINDING = object()
T = TypeVar("T")


Expand Down Expand Up @@ -97,20 +98,19 @@ async def complete(self, messages: list[dict[str, Any]]) -> str:
class Session:
"""A managed agent session.

Note: ``repo_path`` is legacy/backward-compat metadata only;
``execution_binding`` is the authoritative workspace contract.
Note: ``execution_binding`` is the authoritative workspace contract.
``repo_path`` remains backward-compatible metadata and supplies the
default local workspace root when no explicit execution binding is provided.
"""

id: str
created_at: datetime
last_activity: datetime
wire: LocalWire = field(init=False)
approval_store: ApprovalStore = field(default_factory=ApprovalStore)
repo_path: Path | None = None # legacy/backward-compat metadata only
execution_binding: ExecutionBinding = field(
default_factory=lambda: LocalExecutionBinding(
workspace_root=str(Path.cwd().resolve())
)
repo_path: Path | None = None # legacy metadata; seeds default local binding
execution_binding: ExecutionBinding = field( # type: ignore[assignment]
default=cast(ExecutionBinding, _DEFAULT_EXECUTION_BINDING),
)
approval_policy: ApprovalPolicy = ApprovalPolicy.AUTO
provider: Any | None = None
Expand All @@ -131,6 +131,15 @@ class Session:
approval_coordinator: ApprovalCoordinator = field(init=False)

def __post_init__(self) -> None:
if self.execution_binding is cast(ExecutionBinding, _DEFAULT_EXECUTION_BINDING):
workspace_root = (
str(self.repo_path.resolve())
if self.repo_path is not None
else str(Path.cwd().resolve())
)
self.execution_binding = LocalExecutionBinding(
workspace_root=workspace_root
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
self.wire = LocalWire(self.id)
self.approval_coordinator = ApprovalCoordinator(self.approval_store)

Expand All @@ -148,8 +157,8 @@ def to_store_data(self) -> dict[str, Any]:
"id": self.id,
"created_at": self.created_at.isoformat(),
"last_activity": self.last_activity.isoformat(),
# repo_path is legacy/backward-compat metadata; execution_binding is
# the authoritative workspace contract.
# repo_path remains backward-compatible metadata and seeds the
# default local binding when execution_binding is omitted.
"repo_path": None if self.repo_path is None else str(self.repo_path),
"execution_binding": self.execution_binding.to_dict(),
"approval_policy": self.approval_policy.value,
Expand Down Expand Up @@ -187,7 +196,9 @@ def from_store_data(cls, data: dict[str, Any]) -> Session:
execution_binding = ExecutionBinding.from_dict(binding_raw)
else:
workspace_root = (
repo_path_raw if repo_path_raw is not None else str(Path.cwd())
str(Path(repo_path_raw).resolve())
if repo_path_raw is not None
else str(Path.cwd().resolve())
)
execution_binding = LocalExecutionBinding(workspace_root=workspace_root)
session = cls(
Expand Down
65 changes: 65 additions & 0 deletions coding-agent/tests/ui/test_session_manager_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ def test_session_metadata_round_trips_local_execution_binding(tmp_path: Path) ->
assert reloaded.execution_binding.workspace_root == str(bound_repo)


def test_session_defaults_local_execution_binding_from_repo_path(
tmp_path: Path,
) -> None:
bound_repo = tmp_path / "bound-repo"
session = Session(
id="binding-from-repo-path",
created_at=datetime.now(),
last_activity=datetime.now(),
approval_store=ApprovalStore(),
repo_path=bound_repo,
)

assert isinstance(session.execution_binding, LocalExecutionBinding)
assert session.execution_binding.workspace_root == str(bound_repo.resolve())


def test_session_preserves_explicit_execution_binding_with_repo_path(
tmp_path: Path,
) -> None:
bound_repo = tmp_path / "bound-repo"
explicit_root = tmp_path / "explicit-root"
session = Session(
id="explicit-binding-wins",
created_at=datetime.now(),
last_activity=datetime.now(),
approval_store=ApprovalStore(),
repo_path=bound_repo,
execution_binding=LocalExecutionBinding(workspace_root=str(explicit_root)),
)

assert isinstance(session.execution_binding, LocalExecutionBinding)
assert session.execution_binding.workspace_root == str(explicit_root)


def test_session_metadata_defaults_missing_binding_to_local_from_repo_path(
tmp_path: Path,
) -> None:
Expand Down Expand Up @@ -207,6 +241,37 @@ def test_session_metadata_defaults_missing_binding_to_local_from_repo_path(
assert reloaded.execution_binding.workspace_root == str(legacy_repo)


def test_session_metadata_defaults_missing_binding_resolves_relative_repo_path(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
store = InMemorySessionStore()
created_at = datetime.now()
last_activity = datetime.now()
legacy_repo = tmp_path / "legacy-repo"
legacy_repo.mkdir()
monkeypatch.chdir(tmp_path)
store.save(
"legacy-relative-session",
{
"id": "legacy-relative-session",
"created_at": created_at.isoformat(),
"last_activity": last_activity.isoformat(),
"repo_path": "legacy-repo",
"approval_policy": "auto",
"provider_name": None,
"model_name": None,
"base_url": None,
"max_steps": 30,
"tape_id": None,
},
)

reloaded = SessionManager(store=store).get_session("legacy-relative-session")

assert isinstance(reloaded.execution_binding, LocalExecutionBinding)
assert reloaded.execution_binding.workspace_root == str(legacy_repo.resolve())


def test_event_queue_mutations_do_not_persist_sync_runtime_only_state() -> None:
manager = SessionManager(store=InMemorySessionStore())
session = Session(
Expand Down