Merge pull request #1323 from MemPalace/fix/1243-diary-case-insensitive
fix(mcp): case-insensitive agent name in diary read/write (#1243)
This commit is contained in:
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [3.3.5] — unreleased
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **`mempalace_diary_read` silently dropped entries on agent-name case mismatch.** `tool_diary_write` stored the `agent` metadata verbatim after `sanitize_name`, which preserves case, while `tool_diary_read` filtered by exact match. Writing as `"Claude"` and reading as `"claude"` (or vice-versa) returned zero rows. Both endpoints now lowercase `agent_name` immediately after sanitization, so reads are case-insensitive and the default per-agent wing slug is stable across casings. **Behavior change:** entries written prior to this fix under mixed-case agent names will not match the new lowercase filter; run `mempalace repair` if you need to migrate legacy diary metadata. (#1243)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [3.3.4] — unreleased
|
## [3.3.4] — unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+13
-4
@@ -677,7 +677,7 @@ def tool_check_duplicate(content: str, threshold: float = 0.9):
|
|||||||
"vector_disabled": True,
|
"vector_disabled": True,
|
||||||
"vector_disabled_reason": _vector_disabled_reason,
|
"vector_disabled_reason": _vector_disabled_reason,
|
||||||
"hint": (
|
"hint": (
|
||||||
"duplicate detection requires vector search; run " "`mempalace repair` to restore"
|
"duplicate detection requires vector search; run `mempalace repair` to restore"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
@@ -1133,9 +1133,13 @@ def tool_diary_write(agent_name: str, entry: str, topic: str = "general", wing:
|
|||||||
|
|
||||||
This is the agent's personal journal — observations, thoughts,
|
This is the agent's personal journal — observations, thoughts,
|
||||||
what it worked on, what it noticed, what it thinks matters.
|
what it worked on, what it noticed, what it thinks matters.
|
||||||
|
|
||||||
|
Note: ``agent_name`` is normalized to lowercase before storage so
|
||||||
|
that diary reads are case-insensitive (see #1243). "Claude",
|
||||||
|
"claude", and "CLAUDE" all resolve to the same agent.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
agent_name = sanitize_name(agent_name, "agent_name")
|
agent_name = sanitize_name(agent_name, "agent_name").lower()
|
||||||
entry = sanitize_content(entry)
|
entry = sanitize_content(entry)
|
||||||
topic = sanitize_name(topic, "topic")
|
topic = sanitize_name(topic, "topic")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -1144,7 +1148,7 @@ def tool_diary_write(agent_name: str, entry: str, topic: str = "general", wing:
|
|||||||
if wing:
|
if wing:
|
||||||
wing = sanitize_name(wing)
|
wing = sanitize_name(wing)
|
||||||
else:
|
else:
|
||||||
wing = f"wing_{agent_name.lower().replace(' ', '_')}"
|
wing = f"wing_{agent_name.replace(' ', '_')}"
|
||||||
room = "diary"
|
room = "diary"
|
||||||
col = _get_collection(create=True)
|
col = _get_collection(create=True)
|
||||||
if not col:
|
if not col:
|
||||||
@@ -1209,9 +1213,14 @@ def tool_diary_read(agent_name: str, last_n: int = 10, wing: str = ""):
|
|||||||
written to. Diary writes from hooks land in project-derived wings
|
written to. Diary writes from hooks land in project-derived wings
|
||||||
(``wing_<project>``), so requiring a specific wing on read would
|
(``wing_<project>``), so requiring a specific wing on read would
|
||||||
silo those entries from agent-initiated reads.
|
silo those entries from agent-initiated reads.
|
||||||
|
|
||||||
|
Note: ``agent_name`` is normalized to lowercase before filtering so
|
||||||
|
that reads are case-insensitive (see #1243). Entries written under
|
||||||
|
pre-fix mixed-case agent names will not match the lowercase filter;
|
||||||
|
use ``mempalace repair`` to migrate legacy data if needed.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
agent_name = sanitize_name(agent_name, "agent_name")
|
agent_name = sanitize_name(agent_name, "agent_name").lower()
|
||||||
if wing:
|
if wing:
|
||||||
wing = sanitize_name(wing)
|
wing = sanitize_name(wing)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -682,7 +682,8 @@ class TestDiaryTools:
|
|||||||
topic="architecture",
|
topic="architecture",
|
||||||
)
|
)
|
||||||
assert w["success"] is True
|
assert w["success"] is True
|
||||||
assert w["agent"] == "TestAgent"
|
# agent_name is normalized to lowercase on write (#1243).
|
||||||
|
assert w["agent"] == "testagent"
|
||||||
|
|
||||||
r = tool_diary_read(agent_name="TestAgent")
|
r = tool_diary_read(agent_name="TestAgent")
|
||||||
assert r["total"] == 1
|
assert r["total"] == 1
|
||||||
@@ -774,6 +775,50 @@ class TestDiaryTools:
|
|||||||
assert r_scoped["total"] == 1
|
assert r_scoped["total"] == 1
|
||||||
assert r_scoped["entries"][0]["content"] == "project-wing entry"
|
assert r_scoped["entries"][0]["content"] == "project-wing entry"
|
||||||
|
|
||||||
|
def test_diary_read_case_insensitive_agent(self, monkeypatch, config, palace_path, kg):
|
||||||
|
"""Regression for #1243: diary_read must be case-insensitive over
|
||||||
|
agent_name. Writing as "Claude" and reading as "claude" (or vice
|
||||||
|
versa) must surface the same entries — sanitize_name preserved
|
||||||
|
case, which silently dropped reads when the agent name's casing
|
||||||
|
differed from the write."""
|
||||||
|
_patch_mcp_server(monkeypatch, config, kg)
|
||||||
|
_client, _col = _get_collection(palace_path, create=True)
|
||||||
|
del _client
|
||||||
|
from mempalace.mcp_server import tool_diary_read, tool_diary_write
|
||||||
|
|
||||||
|
# Write as "Claude" → read as "claude" should match.
|
||||||
|
w1 = tool_diary_write(
|
||||||
|
agent_name="Claude",
|
||||||
|
entry="entry written as Claude",
|
||||||
|
topic="general",
|
||||||
|
)
|
||||||
|
assert w1["success"]
|
||||||
|
|
||||||
|
r1 = tool_diary_read(agent_name="claude")
|
||||||
|
assert "entries" in r1, r1
|
||||||
|
contents1 = {e["content"] for e in r1["entries"]}
|
||||||
|
assert "entry written as Claude" in contents1
|
||||||
|
|
||||||
|
# Write as "CLAUDE" → read as "Claude" should also match the
|
||||||
|
# same agent. After normalization both writes target the same
|
||||||
|
# lowercase agent identity, so both entries are returned.
|
||||||
|
w2 = tool_diary_write(
|
||||||
|
agent_name="CLAUDE",
|
||||||
|
entry="entry written as CLAUDE",
|
||||||
|
topic="general",
|
||||||
|
)
|
||||||
|
assert w2["success"]
|
||||||
|
|
||||||
|
r2 = tool_diary_read(agent_name="Claude")
|
||||||
|
contents2 = {e["content"] for e in r2["entries"]}
|
||||||
|
assert "entry written as Claude" in contents2
|
||||||
|
assert "entry written as CLAUDE" in contents2
|
||||||
|
|
||||||
|
# The stored agent metadata is the lowercase form, and the
|
||||||
|
# default wing is derived from that lowercase form too.
|
||||||
|
assert w1["agent"] == "claude"
|
||||||
|
assert w2["agent"] == "claude"
|
||||||
|
|
||||||
|
|
||||||
# ── Cache Invalidation (inode/mtime) ──────────────────────────────────
|
# ── Cache Invalidation (inode/mtime) ──────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user