Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integrations/openclaw/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ tool-specific workflow below says to.
- `items` (required): array of `{wing, room, content}`; content must be verbatim
- `diary`: optional `{agent_name, entry, topic?, wing?}`; entry should use AAAK format
- `dedup_threshold`: similarity threshold (default 0.9)
- `added_by`: optional filing agent label (defaults to the diary `agent_name`, else `checkpoint`)
- `mempalace_update_drawer` — Update an existing drawer's content and/or move it to a different wing/room
- `drawer_id` (required)
- `content`, `wing`, `room`: at least one must be provided (no-op otherwise)
Expand Down
25 changes: 23 additions & 2 deletions mempalace/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3898,7 +3898,7 @@ def tool_reconnect():
return {"success": False, "error": str(e)}


def tool_checkpoint(items, diary=None, dedup_threshold=0.9):
def tool_checkpoint(items, diary=None, dedup_threshold=0.9, added_by=None):
"""Batch session save in a single call.

Semantic-dedups each item, files the non-duplicates as drawers, then
Expand All @@ -3909,6 +3909,9 @@ def tool_checkpoint(items, diary=None, dedup_threshold=0.9):

``items`` is a list of ``{"wing", "room", "content"}`` dicts. ``diary``
is an optional ``{"agent_name", "entry", "topic"?, "wing"?}`` dict.
``added_by`` attributes the filed drawers; when omitted it falls back to
the diary's ``agent_name`` (and then to ``"checkpoint"``), so the agent
that filed the session is recorded instead of a generic label.
Reuses the existing single-item handlers so dedup/idempotency/WAL
behaviour is identical to calling them directly.
"""
Expand All @@ -3925,6 +3928,20 @@ def tool_checkpoint(items, diary=None, dedup_threshold=0.9):
out = {"added": [], "duplicates": [], "errors": []}
if not isinstance(items, list):
return {"error": "items must be a list of {wing, room, content} objects"}
# Drawer attribution: an explicit ``added_by`` wins; otherwise fall back to
# the diary's ``agent_name`` (the agent filing this session); otherwise the
# legacy ``"checkpoint"`` label. A blank, whitespace-only, or non-string
# value counts as unspecified at each step, so an empty explicit argument
# still defers to the diary instead of masking it. The chosen name is stored
# verbatim (tool_add_drawer strips lone surrogates but does not case-fold),
# matching how every other caller records ``added_by``; the diary index
# lowercases the same name separately for case-insensitive reads.
resolved_added_by = added_by if isinstance(added_by, str) and added_by.strip() else None
if resolved_added_by is None and isinstance(diary, dict):
agent = diary.get("agent_name")
resolved_added_by = agent if isinstance(agent, str) and agent.strip() else None
if resolved_added_by is None:
resolved_added_by = "checkpoint"
for item in items:
if not isinstance(item, dict):
out["errors"].append({"item": item, "error": "item must be an object"})
Expand All @@ -3947,7 +3964,7 @@ def tool_checkpoint(items, diary=None, dedup_threshold=0.9):
# string by the guard above) we still file rather than drop the
# memory: verbatim recall is the priority and add_drawer's own
# idempotency blocks exact duplicates.
res = tool_add_drawer(wing=wing, room=room, content=content, added_by="checkpoint")
res = tool_add_drawer(wing=wing, room=room, content=content, added_by=resolved_added_by)
if res.get("success"):
out["added"].append(res)
else:
Expand Down Expand Up @@ -4352,6 +4369,10 @@ def tool_checkpoint(items, diary=None, dedup_threshold=0.9):
"type": "number",
"description": "Similarity threshold 0-1 for the per-item dedup check (default 0.9)",
},
"added_by": {
"type": "string",
"description": "Who is filing these drawers. An explicit value takes precedence; otherwise the diary agent_name, else 'checkpoint'.",
},
},
"required": ["items"],
},
Expand Down
199 changes: 199 additions & 0 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,205 @@ def test_checkpoint_registered_in_tools(self):
assert "mempalace_checkpoint" in mcp_server.TOOLS
assert mcp_server.TOOLS["mempalace_checkpoint"]["handler"] is mcp_server.tool_checkpoint

def test_checkpoint_added_by_defaults_to_diary_agent(
self, monkeypatch, config, palace_path, kg
):
"""#2023: with no explicit ``added_by``, each filed drawer is attributed
to the diary ``agent_name`` (verbatim case) rather than the generic
``checkpoint`` label, so the filing agent survives in provenance."""
_patch_mcp_server(monkeypatch, config, kg)
_client, _col = _get_collection(palace_path, create=True)
_client.close() # release file handles; a bare del leaks them on Windows (#1128)
from mempalace.mcp_server import tool_checkpoint

result = tool_checkpoint(
items=[{"wing": "w", "room": "decisions", "content": "Use PostgreSQL for storage."}],
diary={"agent_name": "DeepSeek", "wing": "w", "entry": "SESSION|did.stuff|star"},
)
assert len(result["added"]) == 1

client, col = _get_collection(palace_path)
try:
metas = col.get(include=["metadatas"])["metadatas"]
finally:
client.close()
drawers = [m for m in metas if m.get("room") == "decisions"]
assert len(drawers) == 1
# Verbatim case, not the lowercased diary-index form of agent_name.
assert drawers[0]["added_by"] == "DeepSeek"

def test_checkpoint_explicit_added_by_overrides_diary(self, monkeypatch):
"""An explicit ``added_by`` wins over the diary ``agent_name`` fallback."""
from mempalace import mcp_server

monkeypatch.setattr(
mcp_server, "tool_check_duplicate", lambda *a, **k: {"is_duplicate": False}
)
monkeypatch.setattr(mcp_server, "tool_diary_write", lambda **k: {"success": True})
filed = {}

def _add(**kwargs):
filed.update(kwargs)
return {"success": True, "drawer_id": "d1"}

monkeypatch.setattr(mcp_server, "tool_add_drawer", _add)

mcp_server.tool_checkpoint(
items=[{"wing": "w", "room": "r", "content": "keep me"}],
diary={"agent_name": "deepseek", "entry": "SESSION|x|star"},
added_by="alice",
)
assert filed["added_by"] == "alice"

def test_checkpoint_added_by_falls_back_to_checkpoint_label(self, monkeypatch):
"""Neither an explicit ``added_by`` nor a diary ``agent_name`` -> the
drawer keeps the legacy ``checkpoint`` attribution (backward compatible)."""
from mempalace import mcp_server

monkeypatch.setattr(
mcp_server, "tool_check_duplicate", lambda *a, **k: {"is_duplicate": False}
)
monkeypatch.setattr(mcp_server, "tool_diary_write", lambda **k: {"success": True})
seen = []

def _add(**kwargs):
seen.append(kwargs["added_by"])
return {"success": True, "drawer_id": "d1"}

monkeypatch.setattr(mcp_server, "tool_add_drawer", _add)

# No diary block at all.
mcp_server.tool_checkpoint(items=[{"wing": "w", "room": "r", "content": "a"}])
# Diary present but without an ``agent_name``.
mcp_server.tool_checkpoint(
items=[{"wing": "w", "room": "r", "content": "b"}],
diary={"entry": "SESSION|y|star"},
)
assert seen == ["checkpoint", "checkpoint"]

def test_checkpoint_added_by_accepted_via_dispatch(self, monkeypatch):
"""#2023: ``added_by`` passes the tools/call schema whitelist (the
reporter's HTTP MCP transport reuses this dispatcher) and the real
handler forwards it, for both the explicit value and the diary fallback."""
from mempalace import mcp_server

monkeypatch.setattr(
mcp_server, "tool_check_duplicate", lambda *a, **k: {"is_duplicate": False}
)
monkeypatch.setattr(mcp_server, "tool_diary_write", lambda **k: {"success": True})
filed = {}

def _add(**kwargs):
filed.update(kwargs)
return {"success": True, "drawer_id": "d1"}

monkeypatch.setattr(mcp_server, "tool_add_drawer", _add)

resp = mcp_server.handle_request(
{
"method": "tools/call",
"id": 1,
"params": {
"name": "mempalace_checkpoint",
"arguments": {
"items": [{"wing": "w", "room": "r", "content": "hi"}],
"added_by": "alice",
},
},
}
)
assert "error" not in resp
assert filed["added_by"] == "alice"

filed.clear()
resp2 = mcp_server.handle_request(
{
"method": "tools/call",
"id": 2,
"params": {
"name": "mempalace_checkpoint",
"arguments": {
"items": [{"wing": "w", "room": "r", "content": "yo"}],
"diary": {"agent_name": "DeepSeek", "entry": "SESSION|z|star"},
},
},
}
)
assert "error" not in resp2
assert filed["added_by"] == "DeepSeek"

def test_checkpoint_schema_exposes_added_by(self):
"""``added_by`` is declared in the checkpoint tool schema so the
dispatch whitelist admits it instead of rejecting it as unknown."""
from mempalace import mcp_server

props = mcp_server.TOOLS["mempalace_checkpoint"]["input_schema"]["properties"]
assert "added_by" in props
assert props["added_by"]["type"] == "string"

def test_checkpoint_blank_or_invalid_added_by_defers_to_diary(self, monkeypatch):
"""A blank, whitespace-only, non-string, or None explicit ``added_by``
counts as unspecified, so it defers to the diary ``agent_name`` rather
than masking it; with no usable diary name it falls to ``checkpoint``."""
from mempalace import mcp_server

monkeypatch.setattr(
mcp_server, "tool_check_duplicate", lambda *a, **k: {"is_duplicate": False}
)
monkeypatch.setattr(mcp_server, "tool_diary_write", lambda **k: {"success": True})
seen = []

def _add(**kwargs):
seen.append(kwargs["added_by"])
return {"success": True, "drawer_id": "d1"}

monkeypatch.setattr(mcp_server, "tool_add_drawer", _add)

diary = {"agent_name": "deepseek", "entry": "SESSION|x|star"}
for bad in ("", " ", 123, None):
mcp_server.tool_checkpoint(
items=[{"wing": "w", "room": "r", "content": f"c{bad!r}"}],
diary=diary,
added_by=bad,
)
# Every unusable explicit value defers to the diary agent.
assert seen == ["deepseek", "deepseek", "deepseek", "deepseek"]

# Blank explicit AND a blank diary name -> the legacy label.
seen.clear()
mcp_server.tool_checkpoint(
items=[{"wing": "w", "room": "r", "content": "z"}],
diary={"agent_name": " ", "entry": "SESSION|y|star"},
added_by="",
)
assert seen == ["checkpoint"]

def test_checkpoint_added_by_uniform_across_items(self, monkeypatch):
"""All items in one checkpoint share a single resolved author (a
checkpoint is one agent's session save; attribution is resolved once)."""
from mempalace import mcp_server

monkeypatch.setattr(
mcp_server, "tool_check_duplicate", lambda *a, **k: {"is_duplicate": False}
)
monkeypatch.setattr(mcp_server, "tool_diary_write", lambda **k: {"success": True})
seen = []

def _add(**kwargs):
seen.append(kwargs["added_by"])
return {"success": True, "drawer_id": kwargs["content"]}

monkeypatch.setattr(mcp_server, "tool_add_drawer", _add)

mcp_server.tool_checkpoint(
items=[
{"wing": "w", "room": "r", "content": "one"},
{"wing": "w", "room": "r", "content": "two"},
],
diary={"agent_name": "DeepSeek", "entry": "SESSION|q|star"},
)
assert seen == ["DeepSeek", "DeepSeek"]

def test_get_drawer(self, monkeypatch, config, palace_path, seeded_collection, kg):
_patch_mcp_server(monkeypatch, config, kg)
from mempalace.mcp_server import tool_get_drawer
Expand Down
1 change: 1 addition & 0 deletions website/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Save a whole session in one call. Semantic-dedups each item, files the non-dupli
| `items` | array | **Yes** | Verbatim items to file. Each is `{ wing, room, content }` |
| `diary` | object | No | Diary entry written after filing: `{ agent_name, entry, topic?, wing? }` (`entry` is AAAK-format) |
| `dedup_threshold` | number | No | Similarity threshold 0–1 for the per-item dedup check (default 0.9) |
| `added_by` | string | No | Who is filing these drawers. An explicit value takes precedence; otherwise the diary `agent_name`, else `checkpoint` |

**Returns:** `{ added: [...], duplicates: [...], errors: [...], diary? }`

Expand Down