From dc5c8168d154d4621fb8faefff2a142a64163d94 Mon Sep 17 00:00:00 2001 From: Lumen Yang Date: Tue, 19 May 2026 21:34:08 +0000 Subject: [PATCH] fix(webui): refresh active session on external sidecar updates --- api/models.py | 23 ++++++++++++++++-- api/routes.py | 13 ++++++++--- tests/test_webui_state_db_reconciliation.py | 26 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/api/models.py b/api/models.py index 6ff74869..fe932d28 100644 --- a/api/models.py +++ b/api/models.py @@ -436,7 +436,14 @@ class Session: self.read_only = bool(kwargs.get('read_only', False)) self.enabled_toolsets = enabled_toolsets # List[str] or None — per-session toolset override self.composer_draft = composer_draft if isinstance(composer_draft, dict) else {} - self._metadata_message_count = None + raw_message_count = kwargs.get('message_count') + parsed_message_count = None + if raw_message_count is not None: + try: + parsed_message_count = int(raw_message_count) + except (TypeError, ValueError): + parsed_message_count = None + self._metadata_message_count = parsed_message_count if parsed_message_count is not None and parsed_message_count >= 0 else None @property def path(self): @@ -590,7 +597,19 @@ class Session: parsed['messages'] = [] parsed['tool_calls'] = [] session = cls(**parsed) - session._metadata_message_count = _lookup_index_message_count(sid) + metadata_message_count = _lookup_index_message_count(sid) + if metadata_message_count is None: + raw_count = parsed.get('message_count') + if isinstance(raw_count, int) and raw_count >= 0: + metadata_message_count = raw_count + else: + try: + parsed_count = int(raw_count) + except (TypeError, ValueError): + parsed_count = None + if parsed_count is not None and parsed_count >= 0: + metadata_message_count = parsed_count + session._metadata_message_count = metadata_message_count # Mark this session as a metadata-only stub. save() refuses to write # such a session because doing so would atomically replace the # on-disk JSON with messages=[], wiping the conversation. Any diff --git a/api/routes.py b/api/routes.py index 01671c2a..b3a7c837 100644 --- a/api/routes.py +++ b/api/routes.py @@ -3709,9 +3709,16 @@ def handle_get(handler, parsed) -> bool: _all_msgs = merge_session_messages_append_only(cli_messages, sidecar_messages) else: _all_msgs = merge_session_messages_append_only(getattr(s, "messages", []) or [], state_db_messages) - if not load_messages and state_db_summary: + if not load_messages: sidecar_messages = getattr(s, "messages", []) or [] sidecar_count = len(sidecar_messages) + if sidecar_count == 0: + try: + metadata_count = getattr(s, "_metadata_message_count", None) + if metadata_count is not None: + sidecar_count = max(0, int(metadata_count)) + except (TypeError, ValueError): + sidecar_count = 0 try: sidecar_last = max( float((m or {}).get("timestamp") or 0) @@ -3720,8 +3727,8 @@ def handle_get(handler, parsed) -> bool: ) if sidecar_messages else 0 except (TypeError, ValueError): sidecar_last = 0 - state_count = int(state_db_summary.get("message_count") or 0) - state_last = float(state_db_summary.get("last_message_at") or 0) + state_count = int(state_db_summary.get("message_count") or 0) if state_db_summary else 0 + state_last = float(state_db_summary.get("last_message_at") or 0) if state_db_summary else 0 _all_msgs = sidecar_messages _summary_message_count = max(sidecar_count, state_count) _summary_last_message_at = max(sidecar_last, state_last) diff --git a/tests/test_webui_state_db_reconciliation.py b/tests/test_webui_state_db_reconciliation.py index 01803450..3bb30bd6 100644 --- a/tests/test_webui_state_db_reconciliation.py +++ b/tests/test_webui_state_db_reconciliation.py @@ -135,6 +135,32 @@ def test_api_session_includes_state_db_messages_newer_than_webui_sidecar(monkeyp assert payload["session"]["message_count"] == 4 +def test_metadata_poll_uses_sidecar_message_count_for_external_updates(monkeypatch, tmp_path): + """Active-session external refresh relies on metadata-only counts. + + When no session index exists, metadata-only loads may fall back to + _metadata_message_count=None. The refresh poll must still report the real + sidecar message count; otherwise an external session JSON update can be + invisible until a full reload. + """ + import api.routes as routes + + sid = "webui_reconcile_metadata_sidecar" + sidecar_messages = [ + {"role": "user", "content": "before external update", "timestamp": 1000.0}, + {"role": "assistant", "content": "externally appended", "timestamp": 1001.0}, + ] + _install_test_session(monkeypatch, tmp_path, sid, sidecar_messages) + + handler = _GetHandler(f"/api/session?session_id={sid}&messages=0&resolve_model=0") + routes.handle_get(handler, urlparse(handler.path)) + + assert handler.status == 200 + session = handler.response_json["session"] + assert session["message_count"] == 2 + assert session["last_message_at"] == 1001.0 + + def test_state_db_reconciliation_preserves_sidecar_only_messages(monkeypatch, tmp_path): import api.routes as routes