@@ -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