feat(webui): persist fork boundary metadata
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user