mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-06 02:48:04 +00:00
fix: harden parent-edge repair, detach dangling parentId, trim comments
- Normalize a malformed parent childrenIds (None/str/etc.) before linking so the reverse-edge repair can't raise on a malformed parent. - If parentId points at a missing parent, detach (set None) and log: a dangling ref otherwise re-bricks the frontend walk this PR exists to fix. - Tighten comments to one line where the why is non-obvious; drop the rest. No behaviour change beyond the two fixes above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0ac92e28ab
commit
1c2a71305e
2 changed files with 30 additions and 40 deletions
|
|
@ -567,14 +567,9 @@ class ChatTable:
|
|||
messages = history.setdefault('messages', {})
|
||||
|
||||
def enforce_node_invariants(node: dict) -> dict:
|
||||
# Structural fields are graph invariants, not free-form payload.
|
||||
# A partial streaming/final update must never be able to leave a
|
||||
# node without them, regardless of caller or merge order.
|
||||
# Structural graph fields a partial update must never drop.
|
||||
node['id'] = message_id
|
||||
# The key must always exist (explicit null), otherwise the
|
||||
# frontend's parent walk treats the node as having an unknown
|
||||
# parent and the conversation gets stuck loading.
|
||||
node.setdefault('parentId', None)
|
||||
node.setdefault('parentId', None) # explicit null, never undefined
|
||||
if not isinstance(node.get('childrenIds'), list):
|
||||
node['childrenIds'] = []
|
||||
if not node.get('role'):
|
||||
|
|
@ -586,47 +581,44 @@ class ChatTable:
|
|||
existing = messages.get(message_id)
|
||||
if existing is not None:
|
||||
merged = {**existing, **message}
|
||||
# A partial update must not drop structural fields it simply
|
||||
# omitted, but an explicitly provided value (including a falsy
|
||||
# one such as parentId=None or childrenIds=[]) is intentional
|
||||
# and must win.
|
||||
# Omitted -> keep existing; explicit value (incl. parentId=None,
|
||||
# childrenIds=[]) wins. Empty role / 0 ts still normalized above.
|
||||
for key in ('parentId', 'role', 'timestamp', 'childrenIds'):
|
||||
if key not in message and key in existing:
|
||||
merged[key] = existing[key]
|
||||
messages[message_id] = enforce_node_invariants(merged)
|
||||
else:
|
||||
# The target node does not exist. This upsert is also used for
|
||||
# partial streaming/final updates that assume an assistant
|
||||
# placeholder already exists; if a concurrent whole-chat write
|
||||
# dropped it, synthesize a structurally valid node instead of
|
||||
# persisting the partial payload as a malformed history node.
|
||||
# Creating a node through an upsert is only anomalous when the
|
||||
# payload is itself partial (the lost-placeholder race); a
|
||||
# complete create-through-upsert is legitimate and must not spam
|
||||
# warning-level telemetry.
|
||||
# Node missing: a concurrent whole-chat write likely dropped the
|
||||
# placeholder. Warn only when the payload is itself partial.
|
||||
is_partial = not all(k in message for k in ('id', 'parentId', 'childrenIds', 'role'))
|
||||
if is_partial:
|
||||
log.warning(
|
||||
'upsert_message_to_chat_by_id_and_message_id: creating '
|
||||
f'missing message node {message_id} from a partial '
|
||||
f'payload (role={message.get("role")!r}); an assistant '
|
||||
'placeholder was likely lost by a concurrent chat write'
|
||||
f'upsert: synthesizing lost message node {message_id} '
|
||||
f'from partial payload (role={message.get("role")!r})'
|
||||
)
|
||||
else:
|
||||
log.debug(
|
||||
'upsert_message_to_chat_by_id_and_message_id: creating '
|
||||
f'missing message node {message_id} from a complete payload'
|
||||
)
|
||||
log.debug(f'upsert: creating message node {message_id}')
|
||||
messages[message_id] = enforce_node_invariants({**message})
|
||||
|
||||
# Keep the parent's forward edge consistent with the child's
|
||||
# parentId: the same stale-write class that drops a node can also
|
||||
# drop its entry from the parent's childrenIds.
|
||||
# Keep the graph bidirectional; the same stale-write class can drop
|
||||
# the parent's child link or the parent node itself.
|
||||
parent_id = messages[message_id].get('parentId')
|
||||
if parent_id and isinstance(messages.get(parent_id), dict):
|
||||
siblings = messages[parent_id].setdefault('childrenIds', [])
|
||||
if message_id not in siblings:
|
||||
siblings.append(message_id)
|
||||
if parent_id:
|
||||
parent = messages.get(parent_id)
|
||||
if isinstance(parent, dict):
|
||||
children_ids = parent.get('childrenIds')
|
||||
if not isinstance(children_ids, list):
|
||||
children_ids = []
|
||||
parent['childrenIds'] = children_ids
|
||||
if message_id not in children_ids:
|
||||
children_ids.append(message_id)
|
||||
else:
|
||||
# Dangling parentId re-bricks the frontend walk; detach.
|
||||
log.warning(
|
||||
f'upsert: message {message_id} references missing parent '
|
||||
f'{parent_id}; detaching to keep history loadable'
|
||||
)
|
||||
messages[message_id]['parentId'] = None
|
||||
|
||||
history['currentId'] = message_id
|
||||
|
||||
|
|
|
|||
|
|
@ -3583,11 +3583,9 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
model_id = form_data.get('model', '')
|
||||
|
||||
def build_assistant_message_update(**fields):
|
||||
# Only carry the fields that repair a lost assistant placeholder.
|
||||
# childrenIds and timestamp are deliberately omitted: forcing them
|
||||
# here would clobber a real children list or the original creation
|
||||
# time on an already-existing node through the upsert merge.
|
||||
# chats.py synthesizes those only when the node is actually missing.
|
||||
# Only placeholder-repair fields; childrenIds/timestamp omitted so
|
||||
# the upsert merge can't clobber an existing node (chats.py
|
||||
# synthesizes them when the node is actually missing).
|
||||
update = {
|
||||
'id': metadata['message_id'],
|
||||
'role': 'assistant',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue