Merge #3994 — materialize CLI sessions on rename/move/update (#3985)

This commit is contained in:
nesquena-hermes committed 2026-06-11 21:35:13 +00:00
commit 89fbc898f3
1 file changed
+78 -4
+78 -4
View File
@@ -1568,6 +1568,75 @@ def _ensure_full_session_before_mutation(sid: str, session):
return full_session
def _get_or_materialize_session(sid: str):
"""Get a session, materializing from CLI/agent metadata if not in WebUI store.
Mirrors the fallback logic in /api/session/archive (routes.py:~8530).
Raises:
KeyError: session not found in any store
PermissionError: session is read-only (messaging/Claude Code)
"""
try:
s = get_session(sid)
s = _ensure_full_session_before_mutation(sid, s)
return s
except KeyError:
pass
# Fallback: try to materialize from CLI/agent session metadata
cli_meta = _lookup_cli_session_metadata(sid)
if not cli_meta:
raise KeyError(sid)
# Read-only guard: messaging sessions and Claude Code imports cannot be mutated
if cli_meta.get("read_only"):
raise PermissionError("read-only imported session")
# Preserve source metadata fields
def _apply_source_meta(s):
s.is_cli_session = True
s.source_tag = cli_meta.get("source_tag")
s.raw_source = cli_meta.get("raw_source") or cli_meta.get("source_tag")
s.session_source = cli_meta.get("session_source")
s.source_label = cli_meta.get("source_label")
s.user_id = cli_meta.get("user_id")
s.chat_id = cli_meta.get("chat_id")
s.chat_type = cli_meta.get("chat_type")
s.thread_id = cli_meta.get("thread_id")
s.session_key = cli_meta.get("session_key")
s.platform = cli_meta.get("platform")
if _is_messaging_session_record(cli_meta):
# Messaging sessions: lightweight Session with no messages (state.db is source of truth)
s = Session(
session_id=sid,
title=cli_meta.get("title") or title_from(get_cli_session_messages(sid), "CLI Session"),
workspace=get_last_workspace(),
model=cli_meta.get("model") or "unknown",
created_at=cli_meta.get("created_at"),
updated_at=cli_meta.get("updated_at"),
)
_apply_source_meta(s)
s.save(touch_updated_at=False)
else:
# Regular CLI/agent sessions: import full message history
msgs = get_cli_session_messages(sid)
if not msgs:
raise KeyError(sid)
s = import_cli_session(
sid,
cli_meta.get("title") or title_from(msgs, "CLI Session"),
msgs,
cli_meta.get("model") or "unknown",
profile=cli_meta.get("profile"),
created_at=cli_meta.get("created_at"),
updated_at=cli_meta.get("updated_at"),
)
_apply_source_meta(s)
return s
def _reconcile_stale_stream_state_for_session_rows(session_rows) -> bool:
"""Clear stale persisted stream fields before /api/sessions serializes rows."""
changed = False
@@ -7418,10 +7487,11 @@ def handle_post(handler, parsed) -> bool:
except ValueError as e:
return bad(handler, str(e))
try:
s = get_session(body["session_id"])
s = _ensure_full_session_before_mutation(body["session_id"], s)
s = _get_or_materialize_session(body["session_id"])
except KeyError:
return bad(handler, "Session not found", 404)
except PermissionError:
return bad(handler, "Read-only imported sessions cannot be renamed from WebUI", 403)
with _get_session_agent_lock(body["session_id"]):
from api.session_ops import apply_session_title_rename
apply_session_title_rename(s, body["title"])
@@ -7608,9 +7678,11 @@ def handle_post(handler, parsed) -> bool:
except ValueError as e:
return bad(handler, str(e))
try:
s = get_session(body["session_id"])
s = _get_or_materialize_session(body["session_id"])
except KeyError:
return bad(handler, "Session not found", 404)
except PermissionError:
return bad(handler, "Read-only imported sessions cannot be updated from WebUI", 403)
old_ws = getattr(s, "workspace", "")
old_model = getattr(s, "model", None)
old_provider = getattr(s, "model_provider", None)
@@ -8592,9 +8664,11 @@ def handle_post(handler, parsed) -> bool:
except ValueError as e:
return bad(handler, str(e))
try:
s = get_session(body["session_id"])
s = _get_or_materialize_session(body["session_id"])
except KeyError:
return bad(handler, "Session not found", 404)
except PermissionError:
return bad(handler, "Read-only imported sessions cannot be moved from WebUI", 403)
# #1614: refuse moves into a project owned by another profile.
target_pid = body.get("project_id") or None
if target_pid: