From bb1acec8c0361428867cf5dccf51ee546b1080f7 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Thu, 23 Apr 2026 12:53:28 +0800 Subject: [PATCH 1/3] fix(ui): bind sessions to repo path Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../src/coding_agent/ui/session_manager.py | 21 +++++++++--- .../ui/test_session_manager_public_api.py | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/coding-agent/src/coding_agent/ui/session_manager.py b/coding-agent/src/coding_agent/ui/session_manager.py index 2ac09107f..c00b12dcc 100644 --- a/coding-agent/src/coding_agent/ui/session_manager.py +++ b/coding-agent/src/coding_agent/ui/session_manager.py @@ -107,11 +107,7 @@ class Session: 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()) - ) - ) + execution_binding: ExecutionBinding | None = None approval_policy: ApprovalPolicy = ApprovalPolicy.AUTO provider: Any | None = None provider_name: str | None = None @@ -131,6 +127,15 @@ class Session: approval_coordinator: ApprovalCoordinator = field(init=False) def __post_init__(self) -> None: + if self.execution_binding is None: + 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 + ) self.wire = LocalWire(self.id) self.approval_coordinator = ApprovalCoordinator(self.approval_store) @@ -144,6 +149,8 @@ def as_dict(self) -> dict[str, Any]: } def to_store_data(self) -> dict[str, Any]: + if self.execution_binding is None: + raise RuntimeError("session execution_binding was not initialized") return { "id": self.id, "created_at": self.created_at.isoformat(), @@ -758,6 +765,10 @@ def _persist_session(self, session: Session) -> None: self._store.save(session.id, cast(dict[str, Any], session.to_store_data())) def _resolve_workspace_root(self, session: Session) -> Path: + if session.execution_binding is None: + raise RuntimeError( + f"session {session.id} execution_binding was not initialized" + ) return self._binding_resolver.resolve_workspace_root(session.execution_binding) def _invalidate_runtime(self, session: Session) -> None: diff --git a/coding-agent/tests/ui/test_session_manager_public_api.py b/coding-agent/tests/ui/test_session_manager_public_api.py index 4dc90c1ba..e9b4a17e5 100644 --- a/coding-agent/tests/ui/test_session_manager_public_api.py +++ b/coding-agent/tests/ui/test_session_manager_public_api.py @@ -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: From 2c8fad51d6233765f9530757478cf2e2cc526d53 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Fri, 24 Apr 2026 01:14:39 +0800 Subject: [PATCH 2/3] fix(ui): tighten session binding defaults Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../src/coding_agent/ui/session_manager.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/coding-agent/src/coding_agent/ui/session_manager.py b/coding-agent/src/coding_agent/ui/session_manager.py index c00b12dcc..c4d3bfff4 100644 --- a/coding-agent/src/coding_agent/ui/session_manager.py +++ b/coding-agent/src/coding_agent/ui/session_manager.py @@ -52,6 +52,7 @@ logger = logging.getLogger(__name__) _CHECKPOINT_SESSION_CONFIG_KEY = "session_restart_config" +_DEFAULT_EXECUTION_BINDING = object() T = TypeVar("T") @@ -97,8 +98,9 @@ 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 @@ -106,8 +108,10 @@ class Session: 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 | None = None + 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 provider_name: str | None = None @@ -127,7 +131,7 @@ class Session: approval_coordinator: ApprovalCoordinator = field(init=False) def __post_init__(self) -> None: - if self.execution_binding is 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 @@ -149,14 +153,12 @@ def as_dict(self) -> dict[str, Any]: } def to_store_data(self) -> dict[str, Any]: - if self.execution_binding is None: - raise RuntimeError("session execution_binding was not initialized") return { "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, @@ -765,10 +767,6 @@ def _persist_session(self, session: Session) -> None: self._store.save(session.id, cast(dict[str, Any], session.to_store_data())) def _resolve_workspace_root(self, session: Session) -> Path: - if session.execution_binding is None: - raise RuntimeError( - f"session {session.id} execution_binding was not initialized" - ) return self._binding_resolver.resolve_workspace_root(session.execution_binding) def _invalidate_runtime(self, session: Session) -> None: From 1e8dcf27abddb789ab2cbd9cb3ff5b76cff9a054 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Fri, 24 Apr 2026 01:43:10 +0800 Subject: [PATCH 3/3] fix(ui): normalize legacy session hydration Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../src/coding_agent/ui/session_manager.py | 4 ++- .../ui/test_session_manager_public_api.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/coding-agent/src/coding_agent/ui/session_manager.py b/coding-agent/src/coding_agent/ui/session_manager.py index c4d3bfff4..165fa540a 100644 --- a/coding-agent/src/coding_agent/ui/session_manager.py +++ b/coding-agent/src/coding_agent/ui/session_manager.py @@ -196,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( diff --git a/coding-agent/tests/ui/test_session_manager_public_api.py b/coding-agent/tests/ui/test_session_manager_public_api.py index e9b4a17e5..44be6937a 100644 --- a/coding-agent/tests/ui/test_session_manager_public_api.py +++ b/coding-agent/tests/ui/test_session_manager_public_api.py @@ -241,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(