Skip to content

Commit 3c7bfce

Browse files
authored
Merge branch 'main' into openhands/issue-13971-confirmation-mode-critical
2 parents f54acb8 + e9fb43d commit 3c7bfce

File tree

2 files changed

+430
-9
lines changed

2 files changed

+430
-9
lines changed

openhands-sdk/openhands/sdk/agent/acp_agent.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -752,12 +752,22 @@ def init_state(
752752

753753
self._initialized = True
754754

755-
# Store agent info in agent_state so it's accessible from remote
756-
# conversations (PrivateAttrs aren't serialized in state updates).
755+
# Persist agent info + the ACP session id + its cwd in agent_state.
756+
# Keeping these here (rather than on the frozen ACPAgent model) means
757+
# ConversationState's existing base_state.json persistence carries
758+
# them across agent-server restarts, and ``_start_acp_server`` on the
759+
# next launch reads them back to call ``load_session`` instead of
760+
# starting from scratch. We record ``acp_session_cwd`` alongside the
761+
# id because ACP servers key their persistence by ``cwd``: resuming
762+
# in a different working directory would at best silently miss the
763+
# prior session and at worst load a different session that happens to
764+
# exist at the new cwd.
757765
state.agent_state = {
758766
**state.agent_state,
759767
"acp_agent_name": self._agent_name,
760768
"acp_agent_version": self._agent_version,
769+
"acp_session_id": self._session_id,
770+
"acp_session_cwd": self._working_dir,
761771
}
762772

763773
def _start_acp_server(self, state: ConversationState) -> None:
@@ -777,6 +787,27 @@ def _start_acp_server(self, state: ConversationState) -> None:
777787

778788
working_dir = str(state.workspace.working_dir)
779789

790+
# Prior ACP session id — survives agent-server restarts via
791+
# ConversationState.agent_state (serialized into base_state.json).
792+
# Its presence is the signal to resume; its absence means fresh start.
793+
# ACP servers key persistence by ``cwd``; if the workspace moved we
794+
# drop the id so we don't accidentally resume (or silently load) a
795+
# session the server associates with a different directory.
796+
prior_session_id: str | None = state.agent_state.get("acp_session_id")
797+
prior_session_cwd: str | None = state.agent_state.get("acp_session_cwd")
798+
if prior_session_id is not None and prior_session_cwd not in (
799+
None,
800+
working_dir,
801+
):
802+
logger.warning(
803+
"ACP session %s was created with cwd=%s; current cwd=%s differs, "
804+
"starting a fresh session instead of resuming",
805+
prior_session_id,
806+
prior_session_cwd,
807+
working_dir,
808+
)
809+
prior_session_id = None
810+
780811
async def _init() -> tuple[Any, Any, Any, str, str, str]:
781812
# Spawn the subprocess directly so we can install a
782813
# filtering reader that skips non-JSON-RPC lines some
@@ -845,14 +876,43 @@ async def _init() -> tuple[Any, Any, Any, str, str, str]:
845876
[m.id for m in auth_methods],
846877
)
847878

848-
# Build _meta content for session options (e.g. model selection).
849-
# Extra kwargs to new_session() become the _meta dict in the
850-
# JSON-RPC request — do NOT wrap in _meta= (that double-nests).
851-
session_meta = _build_session_meta(agent_name, self.acp_model)
879+
# Resume the prior ACP session if we have its id. If the server
880+
# has forgotten it (state wiped, new host, etc.) fall through to
881+
# new_session so the conversation still starts cleanly.
882+
#
883+
# We only swallow ACPRequestError here: that is the protocol-level
884+
# "I don't know this session" signal and is recoverable by
885+
# starting fresh. Transport failures (broken pipe, EOF, timeout,
886+
# subprocess crash) propagate — there is no working connection to
887+
# fall back on, and the outer init_state handler cleans up.
888+
session_id: str | None = None
889+
if prior_session_id is not None:
890+
try:
891+
await conn.load_session(
892+
cwd=working_dir,
893+
session_id=prior_session_id,
894+
mcp_servers=[],
895+
)
896+
session_id = prior_session_id
897+
logger.info(
898+
"Resumed ACP session: %s (cwd=%s)",
899+
session_id,
900+
working_dir,
901+
)
902+
except ACPRequestError as e:
903+
logger.warning(
904+
"ACP load_session(%s) failed (%s); starting a fresh session",
905+
prior_session_id,
906+
e,
907+
)
852908

853-
# Create a new session
854-
response = await conn.new_session(cwd=working_dir, **session_meta)
855-
session_id = response.session_id
909+
if session_id is None:
910+
# Build _meta content for session options (e.g. model selection).
911+
# Extra kwargs to new_session() become the _meta dict in the
912+
# JSON-RPC request — do NOT wrap in _meta= (that double-nests).
913+
session_meta = _build_session_meta(agent_name, self.acp_model)
914+
response = await conn.new_session(cwd=working_dir, **session_meta)
915+
session_id = response.session_id
856916
await _maybe_set_session_model(
857917
conn,
858918
agent_name,

0 commit comments

Comments
 (0)