#2151·MetaGPT

Pending role messages can be lost across Team serialize/deserialize recovery

Author: hunter3x3-techCreated Sep 11, 2026Updated Sep 11, 2026

Bug description

Pending messages in a Role's private msg_buffer appear to be lost when a Team is serialized and later deserialized.

RoleContext.msg_buffer is currently defined with exclude=True, so pending messages in that queue are not included in normal Team serialization.

This matters because a message can already have been successfully delivered to a Role, but not yet consumed by _observe() when a checkpoint is created.

Example sequence

Consider two roles, A and B:

  1. A message is delivered to Role B.
  2. The message is present in B.rc.msg_buffer.
  3. Role B has not called _observe() yet, so the message is not yet in B's memory.
  4. The Team is serialized.
  5. The Team is deserialized.
  6. B receives a newly constructed empty msg_buffer.
  7. The previously delivered but unobserved message is no longer available.

In simplified form:

message delivered
    ↓
present in msg_buffer
    ↓
not observed yet
    ↓
serialize / deserialize
    ↓
message disappears

This is particularly relevant to failure recovery, where Team serialization can be used to preserve state after an exception.

Why this matters

The lost message may represent work that was already successfully handed off to another Role.

After recovery, the Role may have:

news = empty
todo = empty
msg_buffer = empty

This can make the Role appear idle even though work was pending before the checkpoint.

As a result, recovery may complete without an obvious serialization error while silently dropping an in-flight handoff.

Relevant implementation details

RoleContext.msg_buffer is currently excluded from serialization:

python
msg_buffer: MessageQueue = Field(
    default_factory=MessageQueue,
    exclude=True,
)

Role._observe() later consumes pending messages from this queue:

python
if not news:
    news = self.rc.msg_buffer.pop_all()

Team.serialize() persists the model state, while deserialization reconstructs a new Team from that serialized state.

There does not appear to be a separate restoration step for pending msg_buffer contents.

MessageQueue already provides dump() and load() functionality, so most of the required serialization mechanism may already exist.

Suggested fix

Persist each Role's pending receive queue as explicit recovery state rather than relying on normal Pydantic field serialization.

One possible approach:

  1. Before writing the Team checkpoint, serialize each Role's current msg_buffer.
  2. Store the pending queue alongside that Role's recoverable state.
  3. During Team/Role deserialization, restore the queue before execution resumes.
  4. Preserve message IDs and queue ordering.
  5. Do not move pending messages into normal Role memory merely to make them serializable, because those messages have not yet been observed by the Role.

Conceptually:

Role checkpoint
├── normal serialized role state
└── pending_messages
    ├── message 1
    ├── message 2
    └── ...

Recovery would then be:

deserialize Role
    ↓
restore pending msg_buffer
    ↓
resume normal _observe()

Regression test

A deterministic regression test should not require an LLM.

For example:

python
# Create a Team and Role.
# Deliver a message to the Role.
# Confirm that the message is pending but has not yet been observed.

assert not role.rc.msg_buffer.empty()
assert message not in role.rc.memory.get()

team.serialize(tmp_path)

restored = Team.deserialize(tmp_path, context=...)

restored_role = ...  # obtain the corresponding restored role

# Pending work should survive the checkpoint boundary.
assert not restored_role.rc.msg_buffer.empty()

pending = restored_role.rc.msg_buffer.pop_all()

assert len(pending) == 1
assert pending[0].id == message.id
assert pending[0].content == message.content

It would also be useful to test several pending messages and verify that queue ordering survives recovery:

before checkpoint: [M1, M2, M3]
after recovery:    [M1, M2, M3]

A second regression could cover the failure-recovery path:

Role A fails
+
Role B has an unread pending message
        ↓
checkpoint
        ↓
restore
        ↓
Role B still receives the pending message

Expected behavior

A Team recovery checkpoint should preserve messages that were successfully delivered to a Role but had not yet been consumed by that Role.

Actual behavior

Pending messages stored only in RoleContext.msg_buffer are excluded from the serialized Team state and can therefore be absent after deserialization.

Source: FoundationAgents/MetaGPT