From 0bdf89f64b00f3e2e885cf00f24ef62bdc7f5ddb Mon Sep 17 00:00:00 2001 From: daichiyasunami-vottia Date: Thu, 3 Sep 2026 20:03:02 +0900 Subject: [PATCH] fix(ingest): make memory-doc filenames unique so concurrent saves do not overwrite save_query_result named files query__.md and wrote them with write_text, so two saves in the same second whose questions share the first 50 characters resolved to one path and the later one silently replaced the earlier. Both calls returned normally. This is the common case when several agents sweep one subsystem in parallel. Add a short uuid to the name. The query_ prefix and .md suffix are unchanged so reflect.load_memory_docs and existing tests keep working. Fixes #3301 --- graphify/ingest.py | 7 ++++++- tests/test_ingest.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/graphify/ingest.py b/graphify/ingest.py index 86b9b7531f..7dc92c3418 100644 --- a/graphify/ingest.py +++ b/graphify/ingest.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import re +import uuid import urllib.error import urllib.parse from datetime import datetime, timezone @@ -299,7 +300,11 @@ def save_query_result( now = datetime.now(timezone.utc) slug = re.sub(r"[^\w]", "_", question.lower())[:50].strip("_") - filename = f"query_{now.strftime('%Y%m%d_%H%M%S')}_{slug}.md" + # A second-granularity stamp plus a 50-char slug is not unique: two saves in + # the same second whose questions share a prefix resolve to one path, and the + # later write_text silently replaces the earlier one (#3301). The short uuid + # makes every save its own file; the query_ prefix and .md suffix are kept. + filename = f"query_{now.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}_{slug}.md" frontmatter_lines = [ "---", diff --git a/tests/test_ingest.py b/tests/test_ingest.py index dd9e17ea8f..6b7d1fb034 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -98,3 +98,15 @@ def test_no_outcome_means_no_outcome_section(tmp_path): def test_invalid_outcome_rejected(tmp_path): with pytest.raises(ValueError): save_query_result("q", "a", tmp_path / "memory", outcome="great") + + +def test_concurrent_saves_of_the_same_question_do_not_overwrite(tmp_path): + """Regression for #3301: a second-granularity stamp plus a 50-char slug is + not unique, so saves in the same second sharing a prefix collapsed into one + file and the earlier ones were silently lost.""" + from concurrent.futures import ThreadPoolExecutor + mem = tmp_path / "memory" + with ThreadPoolExecutor(max_workers=20) as ex: + paths = list(ex.map(lambda _: save_query_result("how does auth work", "a", mem), range(20))) + assert len({p.name for p in paths}) == 20 + assert len(list(mem.glob("*.md"))) == 20