From 4e8029f20e1e6404a1bf04429719222d4ec418ac Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Mon, 19 Oct 2026 10:40:35 +0800 Subject: [PATCH] Limit pending restoration to sessions reloaded from storage --- frontends/desktop_bridge.py | 17 +++++++++----- frontends/tests/test_bridge_hub.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index 9ad82f1c..9bdea2d7 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -246,6 +246,7 @@ class Session: partial: Optional[dict] = None status: str = "idle" # idle|running|error|cancelled agent: Any = None + pending_restore: bool = False # Only records read back from storage await restoration. thread: Optional[threading.Thread] = None cancel_requested: bool = False active_turn_id: str = "" @@ -463,7 +464,8 @@ class AgentManager: plan_path=_sanitize_desktop_plan_path(item["id"], item["plan_path"]), llm_no=item["llm_no"], first_message=meta["first_message"], first_message_at=meta["first_message_at"], - log_refs=meta["log_refs"], has_content=meta["has_content"]) + log_refs=meta["log_refs"], has_content=meta["has_content"], + pending_restore=True) def _load_sessions(self): # New format: one file per session under temp/desktop_sessions/. @@ -762,6 +764,7 @@ class AgentManager: agent.verbose = True threading.Thread(target=agent.run, daemon=True, name=f"GA-{sess.id}").start() sess.agent = agent + sess.pending_restore = False self._sync_display_cache(sess) from frontends import hub hub.connect(agent, f"desktop-{sess.id}", @@ -965,7 +968,7 @@ class AgentManager: "title": sess.title, "cwd": sess.cwd, "status": sess.status, - "loaded": sess.agent is not None, + "loaded": not sess.pending_restore, "createdAt": sess.created_at, "updatedAt": sess.updated_at, "lastError": sess.last_error, @@ -1054,7 +1057,10 @@ class AgentManager: if sess.status == "running": raise web.HTTPConflict(text=json.dumps({"error": "session is already running"}, ensure_ascii=False), content_type="application/json") if sess.agent is None and _e2e_control_token() is None: - self.restore_context(sid) + if sess.pending_restore: + self.restore_context(sid) + else: + self.make_agent(sess) extra = {} if image_ids: extra["image_ids"] = image_ids @@ -1155,7 +1161,8 @@ class AgentManager: emit_session_state(sess, "idle") return if sess.agent is None: - self.restore_context(sess.id) + with self.mutation(): + self.make_agent(sess) agent = sess.agent from desktop_history import preview log_path = getattr(agent, "log_path", None) @@ -1354,7 +1361,7 @@ class AgentManager: sess = self.sessions.get(sid) if not sess: raise web.HTTPNotFound(text=json.dumps({"error": f"session not found: {sid}"}), content_type="application/json") - if sess.agent is not None: + if not sess.pending_restore: return {"ok": True, "sessionId": sid, "restored": False, "loaded": True} try: self.make_agent(sess) diff --git a/frontends/tests/test_bridge_hub.py b/frontends/tests/test_bridge_hub.py index 8d2787ba..7103306b 100644 --- a/frontends/tests/test_bridge_hub.py +++ b/frontends/tests/test_bridge_hub.py @@ -198,3 +198,39 @@ def test_display_cache_is_rebuildable_and_follows_ga(manager): sess.agent.all_outputs.clear() manager._sync_display_cache(sess) assert sess.messages == [] + + +def test_new_session_is_not_pending_restore_until_bridge_reload(manager, monkeypatch): + sess = manager.create_session() + assert sess.agent is None + assert manager.snapshot(sess)['loaded'] is True + def unexpected(*args, **kwargs): + raise AssertionError('normal session must not enter history restoration') + monkeypatch.setattr(manager, 'make_agent', unexpected) + assert manager.restore_context(sess.id)['restored'] is False + manager.set_session_model(sess.id, 1) + assert manager.snapshot(sess)['loaded'] is True + assert 'pending_restore' not in manager._session_dict(sess) + manager.sessions.clear() + manager._load_sessions() + restored = manager.sessions[sess.id] + assert restored.agent is None + assert manager.snapshot(restored)['loaded'] is False + + +def test_new_prompt_initializes_ga_without_restore_path(manager, monkeypatch): + monkeypatch.setattr(sys.modules['plan_state'], 'sync_plan_path_from_text', lambda *a: None, raising=False) + sess = manager.create_session() + monkeypatch.setattr(manager, 'ensure_ga_import_path', lambda: Path(manager.ga_root)) + monkeypatch.setitem(sys.modules, 'agentmain', _make_stub('agentmain', {'GenericAgent': FakeAgent})) + monkeypatch.setattr(hub, 'connect', lambda *a, **kw: None) + def unexpected(*args, **kwargs): + raise AssertionError('new prompt must not enter history restoration') + monkeypatch.setattr(manager, 'restore_context', unexpected) + result = manager.submit_prompt(sess.id, 'hello') + assert result['accepted'] + sess.thread.join(timeout=3) + assert not sess.thread.is_alive() + assert sess.agent is not None + assert manager.snapshot(sess)['loaded'] is True + assert sess.status == 'idle'