feat(webui): persist fork boundary metadata

This commit is contained in:
Xubin Ren
2026-06-10 04:26:06 +08:00
parent 03bca4c0a9
commit 73d4b1cb2f
6 changed files with 182 additions and 8 deletions
+13 -1
View File
@@ -28,7 +28,11 @@ from nanobot.security.workspace_access import (
WorkspaceScopeError,
)
from nanobot.session.goal_state import goal_state_ws_blob
from nanobot.session.webui_turns import websocket_turn_wall_started_at
from nanobot.session.webui_turns import (
WEBUI_TITLE_METADATA_KEY,
clean_generated_title,
websocket_turn_wall_started_at,
)
from nanobot.utils.media_decode import (
FileSizeExceeded,
save_base64_data_url,
@@ -46,6 +50,7 @@ from nanobot.webui.http_utils import (
)
from nanobot.webui.mcp_presets_api import normalize_mcp_preset_mentions
from nanobot.webui.transcript import (
append_fork_marker,
delete_webui_transcript,
fork_transcript_before_user_index,
write_session_messages_as_transcript,
@@ -709,6 +714,13 @@ class WebSocketChannel(BaseChannel):
)
if not transcript_ok:
write_session_messages_as_transcript(target_key, forked.messages)
append_fork_marker(target_key)
fork_title = clean_generated_title(
envelope.get("title") if isinstance(envelope.get("title"), str) else None,
)
if fork_title:
forked.metadata[WEBUI_TITLE_METADATA_KEY] = fork_title
self.gateway.session_manager.save(forked, fsync=True)
except Exception as exc:
delete_webui_transcript(target_key)
self.gateway.session_manager.delete_session(target_key)
+2 -2
View File
@@ -648,8 +648,8 @@ class SessionManager:
``before_user_index`` is zero-based over user messages in the full session:
``0`` means "before the first user message", ``1`` means "before the
second user message", and so on. A value equal to the total user-message
count copies the full session prefix. The target user message itself is
not copied; the WebUI pre-fills it in the composer for editing and resend.
count copies the full session prefix. WebUI assistant-reply forks pass
the next user index so the selected completed assistant turn is included.
"""
if before_user_index < 0:
return None
+46 -1
View File
@@ -17,6 +17,7 @@ from nanobot.config.paths import get_webui_dir
from nanobot.session.manager import SessionManager
WEBUI_TRANSCRIPT_SCHEMA_VERSION = 3
WEBUI_FORK_MARKER_EVENT = "fork_marker"
_MAX_TRANSCRIPT_FILE_BYTES = 8 * 1024 * 1024
_WEBUI_TURN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
WEBUI_TURN_METADATA_KEY = "webui_turn_id"
@@ -306,6 +307,8 @@ def fork_transcript_before_user_index(
user_index = 0
found_target = False
for row in lines:
if row.get("event") == WEBUI_FORK_MARKER_EVENT:
continue
if _is_user_transcript_row(row):
if user_index == before_user_index:
found_target = True
@@ -340,6 +343,17 @@ def fork_transcript_before_user_index(
return True
def append_fork_marker(session_key: str) -> None:
"""Mark the UI-only boundary where a WebUI fork starts accepting new turns."""
append_transcript_object(
session_key,
{
"event": WEBUI_FORK_MARKER_EVENT,
"chat_id": _chat_id_from_session_key(session_key),
},
)
def write_session_messages_as_transcript(
target_key: str,
messages: list[dict[str, Any]],
@@ -1397,6 +1411,28 @@ def replay_transcript_to_ui_messages(
return messages
def fork_boundary_message_count(
lines: list[dict[str, Any]],
*,
augment_user_media: Callable[[list[str]], list[dict[str, Any]]] | None = None,
augment_assistant_media: Callable[[list[str]], list[dict[str, Any]]] | None = None,
augment_assistant_text: Callable[[str], str] | None = None,
) -> int | None:
"""Return the replayed UI message count before the first fork marker, if any."""
for idx, rec in enumerate(lines):
if rec.get("event") != WEBUI_FORK_MARKER_EVENT:
continue
return len(
replay_transcript_to_ui_messages(
lines[:idx],
augment_user_media=augment_user_media,
augment_assistant_media=augment_assistant_media,
augment_assistant_text=augment_assistant_text,
),
)
return None
def build_webui_thread_response(
session_key: str,
*,
@@ -1410,14 +1446,23 @@ def build_webui_thread_response(
if not lines:
return None
lines = inject_missing_user_events_from_session(session_key, lines, session_messages)
fork_boundary = fork_boundary_message_count(
lines,
augment_user_media=augment_user_media,
augment_assistant_media=augment_assistant_media,
augment_assistant_text=augment_assistant_text,
)
msgs = replay_transcript_to_ui_messages(
lines,
augment_user_media=augment_user_media,
augment_assistant_media=augment_assistant_media,
augment_assistant_text=augment_assistant_text,
)
return {
payload = {
"schemaVersion": WEBUI_TRANSCRIPT_SCHEMA_VERSION,
"sessionKey": session_key,
"messages": msgs,
}
if fork_boundary is not None:
payload["fork_boundary_message_count"] = fork_boundary
return payload