Merge pull request #1147 from MemPalace/fix/3.3.3-followups
fix(3.3.3): two followups from #1145 before tag cut
This commit is contained in:
@@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
- Real `python3` resolution for `.sh` hooks with a `MEMPAL_PYTHON` override path. (#833)
|
- Real `python3` resolution for `.sh` hooks with a `MEMPAL_PYTHON` override path. (#833)
|
||||||
- Add optional `wing` parameter to `tool_diary_write` / `tool_diary_read` and derive per-project wing from the Claude Code transcript path when writing from the stop hook — diary entries from different projects no longer collapse into a shared default wing. (#659)
|
- Add optional `wing` parameter to `tool_diary_write` / `tool_diary_read` and derive per-project wing from the Claude Code transcript path when writing from the stop hook — diary entries from different projects no longer collapse into a shared default wing. (#659)
|
||||||
- Treat empty string as "no filter" in `mempalace_search` `wing`/`room`; LLM agents that default to filling every optional parameter with `""` no longer get bounced with `must be a non-empty string`. (#1097, #1084)
|
- Treat empty string as "no filter" in `mempalace_search` `wing`/`room`; LLM agents that default to filling every optional parameter with `""` no longer get bounced with `must be a non-empty string`. (#1097, #1084)
|
||||||
|
- Broaden `_wing_from_transcript_path` to handle Claude Code project folders without a `-Projects-` segment (e.g. `~/dev/<parent>/<project>`, `~/code/<project>`). The project name is now derived from the final dash-separated token of the encoded folder, so Linux users with code outside `~/Projects/` get per-project diary scoping instead of falling through to `wing_sessions`. (#1145, follow-up to #659)
|
||||||
|
- `mempalace_diary_read(wing="")` now returns diary entries from every wing this agent has written to, matching the #1097 "empty-string as no filter" pattern. Previously defaulted to `wing_<agent>`, siloing entries that hooks wrote to project-derived wings. (#1145)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -490,14 +490,29 @@ def _parse_harness_input(data: dict, harness: str) -> dict:
|
|||||||
def _wing_from_transcript_path(transcript_path: str) -> str:
|
def _wing_from_transcript_path(transcript_path: str) -> str:
|
||||||
"""Derive a project wing name from a Claude Code transcript path.
|
"""Derive a project wing name from a Claude Code transcript path.
|
||||||
|
|
||||||
Claude Code stores transcripts at:
|
Claude Code encodes the project's source directory by replacing path
|
||||||
|
separators with dashes, producing folders like:
|
||||||
~/.claude/projects/-home-<user>-Projects-<project>/session.jsonl
|
~/.claude/projects/-home-<user>-Projects-<project>/session.jsonl
|
||||||
We extract <project> and return ``wing_<project>`` to match the
|
~/.claude/projects/-home-<user>-dev-<parent>-<project>/session.jsonl
|
||||||
AAAK_SPEC convention (``wing_user``, ``wing_agent``, ``wing_code``,
|
~/.claude/projects/-Users-<user>-<folder>-<project>/session.jsonl
|
||||||
``wing_<project>``…). Falls back to ``wing_sessions``.
|
|
||||||
|
The project directory name is the final dash-separated token of the
|
||||||
|
encoded folder. Returns ``wing_<project>`` (lowercased, spaces → ``_``).
|
||||||
|
Falls back to ``wing_sessions`` if the path does not match a Claude Code
|
||||||
|
project-folder layout.
|
||||||
"""
|
"""
|
||||||
# Normalize path separators for cross-platform (Windows backslashes)
|
# Normalize path separators for cross-platform (Windows backslashes)
|
||||||
normalized = transcript_path.replace("\\", "/")
|
normalized = transcript_path.replace("\\", "/")
|
||||||
|
# Primary: pull the encoded project folder out of ``.claude/projects/``
|
||||||
|
# and take its last dash-separated token.
|
||||||
|
match = re.search(r"/\.claude/projects/-([^/]+)", normalized)
|
||||||
|
if match:
|
||||||
|
encoded = match.group(1)
|
||||||
|
project = encoded.rsplit("-", 1)[-1]
|
||||||
|
if project:
|
||||||
|
return f"wing_{project.lower().replace(' ', '_')}"
|
||||||
|
# Legacy fallback: explicit ``-Projects-<name>`` segment, useful for
|
||||||
|
# transcripts not under the standard Claude Code projects dir.
|
||||||
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
|
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
|
||||||
if match:
|
if match:
|
||||||
project = match.group(1).lower().replace(" ", "_")
|
project = match.group(1).lower().replace(" ", "_")
|
||||||
|
|||||||
+13
-13
@@ -995,10 +995,11 @@ def tool_diary_read(agent_name: str, last_n: int = 10, wing: str = ""):
|
|||||||
Read an agent's recent diary entries. Returns the last N entries
|
Read an agent's recent diary entries. Returns the last N entries
|
||||||
in chronological order — the agent's personal journal.
|
in chronological order — the agent's personal journal.
|
||||||
|
|
||||||
When ``wing`` is provided, reads from that wing instead of the
|
When ``wing`` is provided, reads only from that wing. When ``wing``
|
||||||
agent's default ``wing_<agent_name>`` wing. This lets hooks
|
is empty or omitted, returns entries from every wing this agent has
|
||||||
direct diary reads to a project-specific wing derived from
|
written to. Diary writes from hooks land in project-derived wings
|
||||||
the transcript path.
|
(``wing_<project>``), so requiring a specific wing on read would
|
||||||
|
silo those entries from agent-initiated reads.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
agent_name = sanitize_name(agent_name, "agent_name")
|
agent_name = sanitize_name(agent_name, "agent_name")
|
||||||
@@ -1007,21 +1008,20 @@ def tool_diary_read(agent_name: str, last_n: int = 10, wing: str = ""):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
last_n = max(1, min(last_n, 100))
|
last_n = max(1, min(last_n, 100))
|
||||||
if not wing:
|
|
||||||
wing = f"wing_{agent_name.lower().replace(' ', '_')}"
|
|
||||||
col = _get_collection()
|
col = _get_collection()
|
||||||
if not col:
|
if not col:
|
||||||
return _no_palace()
|
return _no_palace()
|
||||||
|
|
||||||
|
# Build filter: always scope by agent + room=diary. Wing is optional —
|
||||||
|
# when empty, return entries across all wings for this agent (matches
|
||||||
|
# the #1097 empty-string-as-no-filter convention for LLM ergonomics).
|
||||||
|
conditions = [{"room": "diary"}, {"agent": agent_name}]
|
||||||
|
if wing:
|
||||||
|
conditions.insert(0, {"wing": wing})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = col.get(
|
results = col.get(
|
||||||
where={
|
where={"$and": conditions},
|
||||||
"$and": [
|
|
||||||
{"wing": wing},
|
|
||||||
{"room": "diary"},
|
|
||||||
{"agent": agent_name},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
include=["documents", "metadatas"],
|
include=["documents", "metadatas"],
|
||||||
limit=10000,
|
limit=10000,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -324,6 +324,24 @@ def test_wing_from_transcript_path_lowercases():
|
|||||||
assert _wing_from_transcript_path(path) == "wing_myproject"
|
assert _wing_from_transcript_path(path) == "wing_myproject"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wing_from_transcript_path_non_projects_layout():
|
||||||
|
# Linux users with code under ~/dev/, ~/src/, ~/code/ — no -Projects- segment.
|
||||||
|
# Project name is the final dash-separated token of the encoded folder.
|
||||||
|
path = "/home/igor/.claude/projects/-home-igor-dev-MemPalace-mempalace/session.jsonl"
|
||||||
|
assert _wing_from_transcript_path(path) == "wing_mempalace"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wing_from_transcript_path_macos_users_layout():
|
||||||
|
# macOS ~/ layout without a Projects/ segment.
|
||||||
|
path = "/Users/alice/.claude/projects/-Users-alice-code-MyApp/session.jsonl"
|
||||||
|
assert _wing_from_transcript_path(path) == "wing_myapp"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wing_from_transcript_path_nested_deep():
|
||||||
|
path = "/home/bob/.claude/projects/-home-bob-work-clients-acme-frontend/session.jsonl"
|
||||||
|
assert _wing_from_transcript_path(path) == "wing_frontend"
|
||||||
|
|
||||||
|
|
||||||
# --- _log ---
|
# --- _log ---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -740,6 +740,40 @@ class TestDiaryTools:
|
|||||||
assert entry1 in contents
|
assert entry1 in contents
|
||||||
assert entry2 in contents
|
assert entry2 in contents
|
||||||
|
|
||||||
|
def test_diary_read_empty_wing_spans_all_wings(self, monkeypatch, config, palace_path, kg):
|
||||||
|
"""diary_read(wing='') must return entries from every wing this agent
|
||||||
|
wrote to. Hooks write to project-derived wings (#659); a reader that
|
||||||
|
silos by default wing would never see those entries."""
|
||||||
|
_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
|
||||||
|
|
||||||
|
w1 = tool_diary_write(
|
||||||
|
agent_name="TestAgent",
|
||||||
|
entry="default-wing entry",
|
||||||
|
topic="general",
|
||||||
|
)
|
||||||
|
w2 = tool_diary_write(
|
||||||
|
agent_name="TestAgent",
|
||||||
|
entry="project-wing entry",
|
||||||
|
topic="general",
|
||||||
|
wing="wing_someproject",
|
||||||
|
)
|
||||||
|
assert w1["success"] and w2["success"]
|
||||||
|
|
||||||
|
# Empty wing → return both entries
|
||||||
|
r = tool_diary_read(agent_name="TestAgent", wing="")
|
||||||
|
assert r["total"] == 2
|
||||||
|
contents = {e["content"] for e in r["entries"]}
|
||||||
|
assert "default-wing entry" in contents
|
||||||
|
assert "project-wing entry" in contents
|
||||||
|
|
||||||
|
# Explicit wing → return only that wing's entries
|
||||||
|
r_scoped = tool_diary_read(agent_name="TestAgent", wing="wing_someproject")
|
||||||
|
assert r_scoped["total"] == 1
|
||||||
|
assert r_scoped["entries"][0]["content"] == "project-wing entry"
|
||||||
|
|
||||||
|
|
||||||
# ── Cache Invalidation (inode/mtime) ──────────────────────────────────
|
# ── Cache Invalidation (inode/mtime) ──────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1169,7 +1169,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mempalace"
|
name = "mempalace"
|
||||||
version = "3.3.2"
|
version = "3.3.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "chromadb" },
|
{ name = "chromadb" },
|
||||||
|
|||||||
Reference in New Issue
Block a user