[Bug]: Event ChatLog drops message role when peer_id is present
Issue Origin
Observed or reproduced in a real environment
Bug Description
When a captured message has both role and peer_id, the event ChatLog renderer uses only peer_id. In Codex workspace capture, user and assistant messages share the workspace peer, so durable event memory labels both sides as the same speaker and loses who actually produced each message. Raw session data still contains the correct roles, so this is an extraction/rendering attribution defect, not a capture defect.
Steps to Reproduce
- Install OpenViking 0.4.20.
- Construct one
role=usermessage and onerole=assistantmessage with the samepeer_id. - Pass them to
MessageRange([messages]).pretty_print(). - Observe that both rendered lines use only the peer label.
This also occurs with the official Codex plugin because it assigns the active workspace peer to captured messages regardless of role.
Expected Behavior
Durable ChatLog content should preserve both dimensions when both exist, for example:
**user [peer=workspace-agent]**: OWNER_REQUEST
**assistant [peer=workspace-agent]**: AGENT_RESPONSEMessages without peer_id can keep the existing **user** / **assistant** labels.
Actual Behavior
Both lines are attributed to the peer, so the user message appears to have been spoken by the agent:
**workspace-agent**: OWNER_REQUEST
**workspace-agent**: AGENT_RESPONSEThe role is still present in the raw archive and extraction context, but is discarded by the final event renderer.
Minimal Reproducible Example
from openviking.message import Message
from openviking.message.part import TextPart
from openviking.session.memory.memory_updater import MessageRange
messages = [
Message(id="u", role="user", parts=[TextPart(text="OWNER_REQUEST")], peer_id="workspace-agent"),
Message(id="a", role="assistant", parts=[TextPart(text="AGENT_RESPONSE")], peer_id="workspace-agent"),
]
print(MessageRange([messages]).pretty_print())
No server, API, LLM, or memory write is needed to reproduce it.Error Logs
OpenViking Version
0.4.20 (also present on upstream main on 2026-09-16)
Python Version
3.13.5
Operating System
Windows
Model Backend
None
Additional Context
The behavior is deterministic. MessageRange._speaker_for() currently returns message.peer_id or message.role, so role is silently replaced whenever a peer exists. The official Codex capture script assigns the active peer to each message, while the extraction context formatter still carries both [role][peer].
Relevant code:
- https://github.com/volcengine/OpenViking/blob/main/openviking/session/memory/memory_updater.py
- https://github.com/volcengine/OpenViking/blob/main/openviking/session/memory/session_extract_context_provider.py
- https://github.com/volcengine/OpenViking/blob/main/examples/codex-memory-plugin/scripts/ov-session.mjs
A compatible fix would render role plus optional peer, and ensure any adjacent-message merge compares (role, peer_id) rather than only the peer-derived speaker string. Suggested regression cases: same peer across user/assistant; missing peer; different peers; same-role chunk merge; no merge across different roles.
Source: volcengine/OpenViking