Skip to content

Python: Reject oversized MessagePack history records before writing - #9260

Open
Shivam Sharma (ShivamSharma43) wants to merge 4 commits into
microsoft:mainfrom
ShivamSharma43:fix/8633-msgpack-oversized-record
Open

Shivam Sharma (ShivamSharma43) wants to merge 4 commits into
microsoft:mainfrom
ShivamSharma43:fix/8633-msgpack-oversized-record

Conversation

@ShivamSharma43

Copy link
Copy Markdown
Contributor

Motivation & Context

FileHistoryProvider(serialization_format="msgpack") can save a record that its own reader refuses to load. _read_msgpack_messages() rejects records longer than _MAX_MSGPACK_RECORD_BYTES (64 MiB), but save_messages() appended them without checking. After one oversized message, get_messages() raises ValueError for that session, and every later save_messages() fails the same way because it reads the existing history for deduplication first. A single large inline media payload can make an otherwise valid conversation unusable.

Description & Review Guide

  • What are the major changes? In the MessagePack branch of FileHistoryProvider.save_messages(), the batch of new messages is now encoded up front, and each record is checked against _MAX_MSGPACK_RECORD_BYTES before the file is opened. An oversized record raises ValueError naming its size and the limit. Records are then written from the already-encoded bytes, so each message is encoded once.
  • What is the impact of these changes? A rejected batch leaves the history file untouched, including when the oversized record comes after a valid one in the same batch, so the session stays loadable and the save can be retried with a smaller payload. Records exactly at the limit are still accepted, which matches the reader's > limit check. JSON Lines storage and successful MessagePack saves are unchanged.
  • What do you want reviewers to focus on? Whether raising ValueError from save_messages() is the right signal here. It matches what get_messages() already raises for the same record.

Tests added in tests/core/test_sessions.py, using a lowered limit so no large payloads are allocated:

  • test_msgpack_rejects_oversized_batch_without_mutating_history: a batch of a valid record followed by an oversized one is rejected, the file bytes are unchanged, the session still loads, and saving the valid record afterwards works. It fails without the fix.
  • test_msgpack_record_at_size_limit_round_trips: a record exactly at the limit saves and loads.

Related Issue

Fixes #8633

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

🤖 Generated with Claude Code

FileHistoryProvider(serialization_format="msgpack") appended records larger
than the 64 MiB limit that its reader enforces, so one oversized message made
the session unreadable, and every later save failed too because it reads the
history for deduplication first. Encode and check the whole batch before
opening the file, so a rejected batch leaves the existing history untouched
and the call can be retried.

Fixes microsoft#8633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Oct 11, 2026
@eavanvalkenburg

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): 25740710f8e9
Model: gpt-5.6-sol

Overview

The change correctly aligns MessagePack writes with the reader's per-record limit and validates the complete batch before opening the file, with tests covering rollback and the exact-limit boundary. Existing locking, deduplication, and error propagation preserve the intended history contract. However, the preflight materializes every encoded record at once, so an unbounded batch can exhaust shared process memory even when every individual record is valid.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/packages/core/agent_framework/_sessions.py

return
# Encode and check the whole batch first so a record the reader would reject
# never reaches the file and the existing history stays loadable.
records = [_DEFAULT_MSGPACK_ENCODER.encode(message.to_dict()) for message in new_messages]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this comprehension retains the encoding of every record in an unbounded batch, a caller can repeat an individually valid near-64 MiB message and force memory usage to scale with the whole batch, potentially OOMing a shared worker. Please preserve the all-or-nothing preflight while bounding aggregate retained memory independently of batch length, for example with a bounded-memory validation/spooling strategy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in 73cd27c. The preflight no longer keeps the encodings. Each message is encoded into one reused bytearray with Encoder.encode_into, once to check its size and once more when writing, so memory is bounded by the largest record rather than the batch. The whole batch is still checked before the file is opened, so a rejected batch leaves the history untouched.

test_msgpack_batch_save_memory_does_not_grow_with_batch_length saves 16 messages of about 1 MiB each and checks the tracemalloc peak stays under 6 MiB. It peaks at about 3.3 MB with the change and 34.7 MB on 2574071. The existing rollback and exact-limit tests still pass.

The size preflight kept every encoded record of the batch, so memory grew
with batch length. Encode each record into one reused buffer, once to check
the size and once to write, so memory is bounded by the largest record. The
batch is still rejected as a whole before the file is opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eavanvalkenburg

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 2

Result: Findings reported
Scope: 1 net-new commit(s): 73cd27c751ae
Model: gpt-5.6-sol

Overview

This incremental change replaces batch-wide encoded-record retention with a reusable buffer, and its regression test confirms that memory no longer scales with batch length. The existing preflight, exact-limit test, and reader-side length guard remain strong defenses. However, the write pass serializes each mutable message again without validating the bytes actually persisted, which can recreate the oversized-record corruption this PR is intended to prevent.

Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/packages/core/agent_framework/_sessions.py

serialized = _DEFAULT_MSGPACK_ENCODER.encode(message.to_dict())
file_handle.write(len(serialized).to_bytes(self._MSGPACK_RECORD_HEADER_BYTES, "big"))
file_handle.write(serialized)
_DEFAULT_MSGPACK_ENCODER.encode_into(message.to_dict(), record)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preflight validates the first encoding, but this second to_dict() result is the one actually written and its length is never checked. A mutable message or stateful subclass can fit during preflight and exceed the limit here, leaving a record that the reader rejects and making the session unreadable for later loads and saves. Please ensure the bytes written are the same validated bytes, such as through bounded staging, or make this second pass validation rollback-safe so an invalid record cannot remain appended.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 46e1ce2. The write pass now checks each record's length again before writing it, using the same helper as the preflight, so only checked bytes reach the file. If anything fails during the write pass, including a record that grew since the preflight, the file is truncated back to the offset where this call started appending and the error is raised, so a bad record can't stay appended. Memory is still bounded to one reused record buffer.

test_msgpack_rejects_record_that_grows_after_preflight_without_mutating_history uses a message whose to_dict() fits the first time and is over the limit the second time. It comes after a valid message in the batch, so the valid one is written before the failure. The test checks that the file is byte-for-byte unchanged, that history still loads, and that later saves work. On 73cd27c it fails with DID NOT RAISE.

I also merged the latest main (cbe6e6e). The earlier Windows CI failure was the upstream flaky declarative test test_auth_context_binding_survives_workflow_resume[True-True] (packages/declarative/tests/test_invoke_mcp_tool_executor.py:470), which this PR doesn't touch. The other Windows and 3.15 jobs were cancelled after it failed.

… on failure

The preflight checked the first encoding of each message, but the write
pass encoded it again and wrote that result unchecked. A message whose
to_dict() changes between the passes could still append a record the
reader rejects. The write pass now checks each record before writing it,
and any failure truncates the file back to where this call started
appending, so the history stays loadable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
github-app-auth — cbe6e6e2 Deployed Oct 15, 2026 by ShivamSharma43 via add_label #25404
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Oversized MessagePack history records make sessions unreadable

3 participants