Skip to content

Python: [Bug]: Distinct non-English memory topics silently share a file #9205

Description

@ktz03

Observed Behavior

Writing two distinct non-English topic names through MemoryContextProvider's write_memory tool silently merges them into one file. For example, 旅行计划 (travel plans) and 饮食偏好 (food preferences) both resolve to topics/memory-topic.md. The second write returns the first topic's name with both facts in its memories. Deleting the second topic through delete_memory_topic then removes the shared record, so the first topic can no longer be read.

This occurs with one ordinary owner and session, using the public tools and real local Markdown files. It also reproduces with two Japanese topics, two Cyrillic topics, and café / cafè. Ordinary distinct ASCII topics remain separate. A single non-English topic writes and reloads successfully, and writing the exact same topic twice correctly updates one record.

Expected Behavior

Distinct supported human-readable topic names should not silently address the same record. Writing or deleting one of these topics should leave the other topic's facts intact. The public topic parameter is documented as a human-readable name and currently accepts these values without rejecting them; non-English memory selection is also explicitly supported by #7130.

Steps to Reproduce

  1. Prepare a MemoryContextProvider with a MemoryFileStore for one owner.
  2. Invoke write_memory for the two different topic names below.
  3. Reopen the store and list topics: only the first name remains, holding both facts.
  4. Delete the second name, then attempt to read the first name.
  5. Compare with the ASCII pair in the same script, which keeps two records and preserves the first after deletion.

Minimal Reproduction

import asyncio
import json
from tempfile import TemporaryDirectory

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


async def reproduce(topics):
    with TemporaryDirectory() as directory:
        session = AgentSession(session_id="session-1")
        session.state["owner"] = "local-owner"
        store = MemoryFileStore(directory, owner_state_key="owner")
        provider = MemoryContextProvider(store=store, recent_turns=0, max_extractions=0)
        context = SessionContext(
            session_id=session.session_id,
            input_messages=[Message(role="user", contents=["Remember these facts."])],
        )
        await provider.before_run(agent=None, session=session, context=context, state={})
        tools = {tool.name: tool for tool in context.tools}
        for topic, fact in zip(topics, ["Travel by train.", "Prefer vegetarian food."]):
            await tools["write_memory"].invoke(
                arguments={"topic": topic, "memory": fact}, skip_parsing=True
            )
        reopened = MemoryFileStore(directory, owner_state_key="owner")
        records = reopened.list_topics(session, source_id="memory")
        print(json.dumps([record.to_dict() for record in records], ensure_ascii=False))
        await tools["delete_memory_topic"].invoke(
            arguments={"topic": topics[1]}, skip_parsing=True
        )
        try:
            reopened.get_topic(session, source_id="memory", topic=topics[0])
            print("first topic still exists")
        except FileNotFoundError:
            print("first topic is missing")


async def main():
    await reproduce(["Travel", "Food"])
    await reproduce(["旅行计划", "饮食偏好"])


asyncio.run(main())

Error Messages and Stack Traces

No error is raised by either write. The ASCII pair produces two records followed by first topic still exists. The Chinese pair produces one record named 旅行计划, with both facts and slug memory-topic, followed by first topic is missing.

The experimental HARNESS warning is present. No model or external service is required.

Package Versions

Executed the complete tracked core package source from ca936db37708539bd502240a48543f6094259ce5 with cached dependencies. Current main 91ab44faa4824a30247d00c341f3498ba46b1ca3 has identical executable core package source; within this package, pyproject.toml changes the version from 1.20.0 to 1.21.0 and raises optional all extra package requirements. Installed core distribution metadata is 1.20.0. A fresh installation of the 1.21.0 dependency set or optional all extra has not been tested.

Python Version

Python 3.14.3

Operating System

Windows

Regression

Unknown

Additional Context

_slugify_topic() keeps only ASCII letters and digits, using memory-topic if none remain. _merge_memory() looks up the existing record by this slug and appends the new fact without checking whether it is a different topic name. Topic lookup and deletion use the same mapping.

The original memory harness was added in #5613. #7130 fixes Unicode keyword extraction for selecting memories; it does not change topic filenames. Open #9153 preserves explicitly chosen custom slugs during reload while leaving default slug derivation unchanged. This report concerns collisions between automatically derived topic identities, with no custom slug supplied. #9197 concerns contributor session ID serialization, and #9079 concerns summary metadata parsing.

No product patch has been implemented. Please confirm the intended topic identity and compatibility approach before implementation. Already conflated records cannot be reliably separated from their stored contents alone; changing the filename mapping also needs an explicit decision for existing files and lookup by slug.

AI Assistance

AI-assisted issue analysis and reproduction tests.

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 8, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 8, 2026
  3. github-actions commented on Oct 8, 2026

    @github-actions
    Contributor

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

    Agent analysis

    Repro: python/packages/core/agent_framework/_harness/_memory.py::_slugify_topic at lines 141-143 and MemoryFileStore._topic_path at lines 776-777 map distinct topics with identical ASCII projections to one file. Two write_memory calls for 旅行计划 and 饮食偏好 merge both facts, and delete_memory_topic for the latter removes the former. Minimal repro: run test_memory_context_provider_keps_distinct_non_ascii_topics_separate using a temporary MemoryFileStore.

    • Failing test: python/packages/core/tests/core/test_harness_memory.py::test_memory_context_provider_keps_distinct_non_ascii_topics_separate
    • 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_keps_distinct_non_ascii_topics_separate (failed), test_memory_context_provider_tools_and_automation (passed)
    • Reported version: 1.20.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 8, 2026
  5. he-yufeng commented on Oct 9, 2026

    @he-yufeng
    Contributor

    Reproduced on current main (declarative/core at c24ef0a). The collision is exactly where the triage notes point: _slugify_topic strips every non-[a-z0-9] character, so 旅行计划 and 饮食偏好 both fall through to the memory-topic fallback and café/cafè both become caf. Store-level check with one owner/session:

    _slugify_topic("旅行计划") == _slugify_topic("饮食偏好") == "memory-topic"
    _slugify_topic("café") == _slugify_topic("cafè") == "caf"
    # two write_topic calls -> a single topics/memory-topic.md
    # get_topic("旅行计划") returns the second topic's memories
    

    Since the second write clobbers identity and delete_memory_topic then removes the shared file, this is silent data loss for any non-ASCII deployment.

    I'd like to take this. Plan: keep _slugify_topic's readable output byte-identical for topics that are already safe (slug == normalized.lower(), so existing ASCII stems are untouched), and append a short sha256 digest of the normalized topic whenever the mapping discarded information, e.g. café -> caf-4f8b…, 旅行计划 -> memory-topic-9c21…. Same collision-resistance standard as the _storage_key_segment digest fallback already used for owner/source segments in this file. On top of that, get_topic/delete_topic fall back to the legacy lossy path when the new file is absent, and write_topic absorbs a legacy-named file into the new name, so stores written by older versions stay readable and migrate on first rewrite instead of stranding. Regression tests: distinct CJK/Cyrillic/accented topics stay separate, deleting one leaves the other readable, legacy-name read-back, and slug idempotency through MemoryTopicRecord/MemoryIndexEntry re-derivation.

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