Release v0.51.474 — reconcile WebUI session detail source flags (#4348)

Ships @dso2ng's fix: /api/session now reconciles stale CLI source flags
against the state.db WebUI-origin row, and _isExternalSession() refuses to
classify an explicit WebUI-source session as external — so a WebUI-native
transcript with a stale sidecar isn't force-reloaded like an imported one.

Rebased onto master (contributor's net code-diff applied; byte-identical to
PR head). CHANGELOG stamped v0.51.474.

Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-17 05:11:45 +00:00
parent 8d0a3035c4
commit 849aba1c8a
5 changed files with 134 additions and 2 deletions
+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.474] — 2026-06-17 — Release QI (reconcile WebUI session source flags)
### Fixed
- **A WebUI-native session is no longer force-reloaded as if it were an imported CLI transcript (#4348).** When a WebUI-origin session carried a stale sidecar that still said `raw_source=cli` / `is_cli_session=true` (while state.db correctly recorded it as WebUI-origin), `/api/session` returned the stale flags and the browser opened it as an external session — entering the periodic external-refresh reload path that could rebuild the transcript while you were reading it. The detail endpoint now reconciles its source flags against the state.db row (the source of truth) before redaction, and the frontend `_isExternalSession()` refuses to classify an explicit WebUI-source session as external. Keeps the list and detail endpoints on one source-classification contract; imported CLI/messaging sessions are unaffected. Thanks @dso2ng.
## [v0.51.473] — 2026-06-17 — Release QH (clean cancelled user context chains)
### Fixed
+43 -1
View File
@@ -4580,6 +4580,46 @@ def _normalize_sidebar_source_flags(session: dict) -> dict:
return normalized
def _reconcile_session_detail_source_flags(session: dict, state_meta: dict) -> dict:
"""Return a /api/session payload whose source flags match state.db truth.
WebUI-origin sidecars can carry stale CLI/import flags after older repair or
import paths touched the JSON file. The sidebar projection already trusts the
state.db source row for those sessions; the detail endpoint must do the same
or the frontend opens a WebUI-native transcript as an external session and
starts the destructive active-refresh reload loop.
"""
if not isinstance(session, dict):
return session
if not _session_source_is_webui(state_meta):
return dict(session)
reconciled = dict(session)
reconciled["is_cli_session"] = False
reconciled["read_only"] = False
reconciled["source_tag"] = _safe_first(state_meta.get("source_tag"), "webui")
reconciled["raw_source"] = _safe_first(state_meta.get("raw_source"), "webui")
reconciled["session_source"] = _safe_first(state_meta.get("session_source"), "webui")
reconciled["source_label"] = _safe_first(state_meta.get("source_label"), "WebUI")
if state_meta.get("source"):
reconciled["source"] = state_meta["source"]
for key in ("message_count", "actual_message_count"):
if state_meta.get(key) is not None:
reconciled[key] = max(
_numeric_count(reconciled.get(key)),
_numeric_count(state_meta.get(key)),
)
for key in ("created_at", "updated_at", "last_message_at"):
if state_meta.get(key) is not None:
current = reconciled.get(key)
try:
reconciled[key] = max(float(current or 0), float(state_meta.get(key) or 0))
except (TypeError, ValueError):
reconciled[key] = state_meta[key]
return reconciled
def _session_source_is_webui(session: dict) -> bool:
"""Return True for state.db/sidebar rows that describe WebUI-origin sessions."""
if not isinstance(session, dict):
@@ -7205,7 +7245,9 @@ def handle_get(handler, parsed) -> bool:
float(raw.get("updated_at") or 0),
_merged_last_message_at,
)
if cli_meta and _is_messaging_session_record(cli_meta):
if cli_meta and _session_source_is_webui(cli_meta):
raw = _reconcile_session_detail_source_flags(raw, cli_meta)
elif cli_meta and _is_messaging_session_record(cli_meta):
raw = _merge_cli_sidebar_metadata(raw, cli_meta)
# ``message_count`` in /api/session is the display coordinate
# space used for pagination and the header badge. Messaging
+14 -1
View File
@@ -1420,8 +1420,21 @@ function _isMessagingSession(session) {
* before the WebUI can read or send messages into it.
* Covers both legacy CLI sessions and messaging-source sessions.
*/
function _isWebUiSourceSession(session) {
if (!session) return false;
const source = (
session.session_source
|| session.raw_source
|| session.source_tag
|| session.source
|| ''
).toLowerCase();
return source === 'webui';
}
function _isExternalSession(session) {
return !!(session && (session.is_cli_session || _isMessagingSession(session)));
if (!session || _isWebUiSourceSession(session)) return false;
return !!(session.is_cli_session || _isMessagingSession(session));
}
function _externalImportPayload(session) {
@@ -110,6 +110,67 @@ def test_webui_source_overrides_stale_cli_flag_even_with_default_title():
assert _normalize_sidebar_source_flags(stale_webui)["is_cli_session"] is False
def test_webui_state_db_source_overrides_stale_cli_detail_payload():
from api.routes import _reconcile_session_detail_source_flags
detail_payload = {
"session_id": "webui-tip",
"title": "Long WebUI session",
"source_tag": "cli",
"raw_source": "cli",
"session_source": "cli",
"source_label": "CLI",
"is_cli_session": True,
"read_only": True,
"message_count": 24,
}
state_db_row = {
"session_id": "webui-tip",
"source_tag": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_label": "WebUI",
"message_count": 26,
}
reconciled = _reconcile_session_detail_source_flags(detail_payload, state_db_row)
assert reconciled["is_cli_session"] is False
assert reconciled["read_only"] is False
assert reconciled["source_tag"] == "webui"
assert reconciled["raw_source"] == "webui"
assert reconciled["session_source"] == "webui"
assert reconciled["source_label"] == "WebUI"
assert reconciled["message_count"] == 26
def test_real_cli_source_survives_detail_source_reconcile():
from api.routes import _reconcile_session_detail_source_flags
detail_payload = {
"session_id": "cli-tip",
"source_tag": "cli",
"raw_source": "cli",
"session_source": "cli",
"source_label": "CLI",
"is_cli_session": True,
"read_only": True,
}
state_db_row = {
"session_id": "cli-tip",
"source_tag": "cli",
"raw_source": "cli",
"session_source": "cli",
"source_label": "CLI",
}
reconciled = _reconcile_session_detail_source_flags(detail_payload, state_db_row)
assert reconciled["is_cli_session"] is True
assert reconciled["read_only"] is True
assert reconciled["source_tag"] == "cli"
def test_real_cli_sidebar_cli_flag_is_preserved_before_frontend_response():
from api.routes import _normalize_sidebar_source_flags
@@ -22,6 +22,16 @@ def test_active_session_external_refresh_uses_metadata_then_force_reload():
assert "remoteCount > localCount || remoteLast > localLast" in SESSIONS_JS
assert "if(S.busy || S.activeStreamId) return;" in SESSIONS_JS
assert "document.hidden" in SESSIONS_JS
assert "externalRefreshReason:reason||'poll'" in SESSIONS_JS
def test_webui_source_never_counts_as_external_session():
assert "function _isWebUiSourceSession(session)" in SESSIONS_JS
assert "if (!session || _isWebUiSourceSession(session)) return false;" in SESSIONS_JS
external_start = SESSIONS_JS.index("function _isExternalSession(session)")
external_body = SESSIONS_JS[external_start : external_start + 300]
assert "_isWebUiSourceSession(session)" in external_body
assert "session.is_cli_session || _isMessagingSession(session)" in external_body
def test_active_session_external_refresh_has_focus_and_visibility_hooks():