#5104·OpenViking

[Bug]: Event ChatLog drops message role when peer_id is present

Author: n0ffn0ffCreated Sep 16, 2026Updated Sep 17, 2026
Labelsbug

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

  1. Install OpenViking 0.4.20.
  2. Construct one role=user message and one role=assistant message with the same peer_id.
  3. Pass them to MessageRange([messages]).pretty_print().
  4. 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_RESPONSE

Messages 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_RESPONSE

The role is still present in the raw archive and extraction context, but is discarded by the final event renderer.

Minimal Reproducible Example

python
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

bash

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:

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.