Skip to content

Python: [Bug]: Consolidated memory continuation lines disappear on reload #9228

Description

@ktz03

Observed Behavior

consolidate_memories accepts a valid JSON response whose memories array contains a string with an ordinary line break and returns {"consolidated_topics": 1}. A fresh MemoryFileStore then returns only the first line of that memory.

For example, the supplied memory is Deploy staging.\nThen deploy production., but a fresh read returns ['Deploy staging.']. The continuation is still present in the raw Markdown file immediately after consolidation; it is omitted by the record parser. This report concerns that parsed-memory discrepancy, not a model-quality result or a general storage-loss claim.

The same string through the public write_memory tool retains both sentences by normalizing whitespace. A single-line consolidation result round-trips normally. The default consolidation prompt asks for strings in a memories array and does not prohibit line breaks.

Expected Behavior

A successful consolidation should not silently omit part of an accepted memory when the file is read again. Either preserve all of the memory text, normalize it to the existing single-line representation, or reject unsupported formatting while preserving the previous record.

Steps to Reproduce

  1. Create a MemoryFileStore and MemoryContextProvider with the deterministic offline client below.
  2. Write a normal memory using the provider's public tool.
  3. Consolidate with one valid JSON memory containing LF or CRLF.
  4. Open a fresh store and compare the memory with the supplied text. The single-line case is a normal control.

Minimal Reproduction

The client returns an actual SDK ChatResponse; there are no private provider calls, network requests or model calls.

import asyncio
import json
import tempfile

from agent_framework import (
    AgentSession, ChatResponse, MemoryContextProvider, MemoryFileStore,
    Message, SessionContext,
)


class OfflineClient:
    def __init__(self, memory):
        self.memory = memory

    async def get_response(self, messages, **kwargs):
        result = {"summary": "Deployment", "memories": [self.memory]}
        return ChatResponse(messages=[Message(role="assistant", contents=[json.dumps(result)])])


async def reproduce(memory):
    with tempfile.TemporaryDirectory() as directory:
        session = AgentSession(session_id="example")
        session.state["owner"] = "example-owner"
        store = MemoryFileStore(directory, owner_state_key="owner")
        provider = MemoryContextProvider(
            store=store, recent_turns=0, max_extractions=0,
            consolidation_client=OfflineClient(memory),
        )
        context = SessionContext(session_id=session.session_id, input_messages=[])
        await provider.before_run(agent=None, session=session, context=context, state={})
        tools = {tool.name: tool for tool in context.tools}
        await tools["write_memory"].invoke(
            arguments={"topic": "Deployment", "memory": "Deploy staging then production."},
            skip_parsing=True,
        )
        response = await tools["consolidate_memories"].invoke(arguments={}, skip_parsing=True)
        fresh = MemoryFileStore(directory, owner_state_key="owner").get_topic(
            session, source_id="memory", topic="Deployment"
        )
        print({"input": memory, "result": response, "reloaded": fresh.memories})


async def main():
    await reproduce("Deploy staging then production.")
    await reproduce("Deploy staging.\nThen deploy production.")
    await reproduce("Deploy staging.\r\nThen deploy production.")


asyncio.run(main())

Observed output, with the experimental warning retained:

single-line: result={"consolidated_topics": 1}; reloaded=['Deploy staging then production.']
LF:          result={"consolidated_topics": 1}; reloaded=['Deploy staging.']
CRLF:        result={"consolidated_topics": 1}; reloaded=['Deploy staging.']

Package Versions

agent-framework-core source at current main 2d9cc3f8ae465baf1aed3022f89b6848db4ffa7b (pyproject.toml version 1.21.0). All 84 checked package Python files match that commit's Git blobs. The cached dependency environment reports Core 1.20.0 metadata; the imported executable source is the verified main source. No fresh workspace-lock installation or full core-suite claim.

Python Version

Python 3.14.3.

Operating System

Windows.

Regression

Unknown.

Additional Context

The original memory harness and Markdown format belong to #5613. Its review already called for heading-marker round-trip preservation; this observation uses an ordinary continuation line and the accepted consolidation response path. #9078/#9079 concern summary text resembling metadata; #9153 and #9205/#9227 concern topic identity; #9218 concerns consolidation scheduling markers. Their existing scope and authorship are preserved.

Public provider/store/tool controls covered single lines, ordinary LF/CRLF continuations, list continuations and heading controls. A separate independent check confirms LF/CRLF omissions and normal single-line, two-item, list and Unicode controls. Matrices overlap and are not added. No product patch, live model, remote store, other operating system or performance validation.

Please confirm the desired single-line normalization or unsupported-format handling before implementation. This report does not propose a new file format or attempt to reconstruct already omitted content.

AI Assistance

AI-assisted analysis, reproduction, and writing.

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 9, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 9, 2026
  3. github-actions commented on Oct 9, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: python/packages/core/agent_framework/_harness/_memory.py::MemoryTopicRecord.to_markdown/from_markdown loses continuation lines when MemoryContextProvider._consolidate_topic accepts a memory string containing LF or CRLF. Minimal repro: consolidate an existing topic with memories=["Deploy staging.\nThen deploy production."], confirm {"consolidated_topics": 1}, then read it through a fresh MemoryFileStore; only Deploy staging. is returned.

    • Failing test: python/packages/core/tests/core/test_harness_memory.py::test_memory_consolidation_round_trips_multiline_memories
    • Files examined: python/packages/core/agent_framework/_harness/_memory.py, python/packages/core/tests/core/test_harness_memory.py, python/packages/core/pyproject.toml
    • Tests run: test_memory_context_provider_uses_explicit_consolidation_client, test_memory_consolidation_round_trips_multiline_memories
    • Reported version: 1.21.0
    • Current version: 1.21.0
  4. added
    harness[Issues, PRs], Target: harness-level items
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 9, 2026
  5. he-yufeng commented on Oct 9, 2026

    @he-yufeng
    Contributor

    Reproduced on current main (65a12f9, core 1.21.0). The write path already normalizes: _merge_memory runs every memory through _normalize_memory_text, so write_memory never persists a line break. The consolidation path skips that step: _consolidate_topic passes the LLM's memories strings straight into MemoryTopicRecord, to_markdown writes the break verbatim as a non-bullet line, and from_markdown only reads - bullets, so the continuation vanishes on the next load while the raw file still shows it.

    I'd like to take this. Proposed fix: normalize memories in MemoryTopicRecord.__init__ with the same _normalize_memory_text the write path uses (keeping the current drop-empty tolerance), so single-line memories become a record invariant and the markdown round-trip is stable by construction. That covers consolidation, extraction, and any future JSON-sourced path at once, matching the write_memory behavior you noted as the reference. Regression coverage: LF and CRLF consolidation memories read back intact (normalized) through a fresh store, plus a record-level markdown round-trip.

  6. he-yufeng commented on Oct 10, 2026

    @he-yufeng
    Contributor

    This is exactly what #9240 fixes: to_markdown wrote the line break verbatim as a non-bullet line, and the reload path only collected bullet items, so everything after the first break was dropped. The PR makes multi-line memory strings survive the round trip; verified with the same fresh-store reload scenario.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

harness[Issues, PRs], Target: harness-level itemspythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions