mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
fix(openviking): gate memory writes and add viking_forget
Mirror built-in memory writes to external providers only after the native memory tool succeeds and is not staged for approval. Keep OpenViking's built-in memory mirroring add-only, since Hermes native memory entries do not yet have stable OpenViking file URIs for replace/remove. Add a narrow viking_forget tool for exact user memory file deletion and document the current OpenViking write/delete behavior.
This commit is contained in:
parent
38c56a1e86
commit
70e7132e2f
9 changed files with 560 additions and 45 deletions
|
|
@ -1459,6 +1459,115 @@ def test_tool_add_resource_sends_git_remote_sources_as_path(url):
|
|||
})
|
||||
|
||||
|
||||
def test_get_tool_schemas_includes_narrow_forget_tool():
|
||||
provider = OpenVikingMemoryProvider()
|
||||
|
||||
names = [schema["name"] for schema in provider.get_tool_schemas()]
|
||||
|
||||
assert "viking_forget" in names
|
||||
|
||||
|
||||
def test_handle_tool_call_forget_deletes_exact_memory_file_uri():
|
||||
uri = "viking://user/peers/hermes/memories/preferences/mem_abc123.md"
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
provider._client.delete.return_value = {
|
||||
"status": "ok",
|
||||
"result": {"uri": uri, "estimated_deleted_count": 1},
|
||||
}
|
||||
|
||||
result = json.loads(provider.handle_tool_call("viking_forget", {"uri": uri}))
|
||||
|
||||
provider._client.delete.assert_called_once_with(
|
||||
"/api/v1/fs",
|
||||
params={"uri": uri, "recursive": False},
|
||||
)
|
||||
assert result == {
|
||||
"status": "deleted",
|
||||
"uri": uri,
|
||||
"estimated_deleted_count": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_handle_tool_call_forget_deletes_exact_memory_file_under_memories_root():
|
||||
uri = "viking://user/default/memories/profile.md"
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
provider._client.delete.return_value = {
|
||||
"status": "ok",
|
||||
"result": {"uri": uri, "estimated_deleted_count": 1},
|
||||
}
|
||||
|
||||
result = json.loads(provider.handle_tool_call("viking_forget", {"uri": uri}))
|
||||
|
||||
provider._client.delete.assert_called_once_with(
|
||||
"/api/v1/fs",
|
||||
params={"uri": uri, "recursive": False},
|
||||
)
|
||||
assert result == {
|
||||
"status": "deleted",
|
||||
"uri": uri,
|
||||
"estimated_deleted_count": 1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri", [
|
||||
"",
|
||||
"https://example.com/mem.md",
|
||||
"viking:/user/memories/preferences/mem_abc123.md",
|
||||
"viking://resources/project/doc.md",
|
||||
"viking://resources/project/memories/mem_abc123.md",
|
||||
"viking://memories/preferences/mem_abc123.md",
|
||||
"viking://agent/hermes/memories/preferences/mem_abc123.md",
|
||||
"viking://user/skills/example/SKILL.md",
|
||||
"viking://user/sessions/session-1/messages.jsonl",
|
||||
"viking://user/memories/preferences/",
|
||||
"viking://user/memories/preferences/.overview.md",
|
||||
"viking://user/memories/preferences/.abstract.md",
|
||||
"viking://user/memories/preferences/mem_abc123.md?recursive=true",
|
||||
])
|
||||
def test_handle_tool_call_forget_rejects_non_memory_file_uris(uri):
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
|
||||
result = json.loads(provider.handle_tool_call("viking_forget", {"uri": uri}))
|
||||
|
||||
assert "error" in result
|
||||
provider._client.delete.assert_not_called()
|
||||
|
||||
|
||||
def test_viking_client_delete_uses_identity_headers(monkeypatch):
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="test-key",
|
||||
account="acct",
|
||||
user="alice",
|
||||
agent="hermes",
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def capture_delete(url, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["kwargs"] = kwargs
|
||||
return SimpleNamespace(
|
||||
status_code=200,
|
||||
text="",
|
||||
json=lambda: {"status": "ok", "result": {"uri": "viking://user/memories/x.md"}},
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(client._httpx, "delete", capture_delete)
|
||||
|
||||
assert client.delete("/api/v1/fs", params={"uri": "viking://user/memories/x.md"}) == {
|
||||
"status": "ok",
|
||||
"result": {"uri": "viking://user/memories/x.md"},
|
||||
}
|
||||
assert captured["url"] == "https://example.com/api/v1/fs"
|
||||
assert captured["kwargs"]["params"] == {"uri": "viking://user/memories/x.md"}
|
||||
assert captured["kwargs"]["headers"]["Authorization"] == "Bearer test-key"
|
||||
assert captured["kwargs"]["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
|
||||
|
||||
|
||||
def test_viking_client_upload_temp_file_uses_multipart_identity_headers(tmp_path, monkeypatch):
|
||||
sample = tmp_path / "sample.md"
|
||||
sample.write_text("# Local resource\n", encoding="utf-8")
|
||||
|
|
@ -2637,6 +2746,46 @@ def test_on_memory_write_uses_content_write_independent_of_session_rotation():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("action", "content"),
|
||||
[
|
||||
("replace", "updated memory"),
|
||||
("remove", ""),
|
||||
("forget", ""),
|
||||
("delete", ""),
|
||||
],
|
||||
)
|
||||
def test_on_memory_write_ignores_non_add_actions(action, content, monkeypatch):
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
provider._endpoint = "http://test"
|
||||
provider._api_key = ""
|
||||
provider._account = "acct"
|
||||
provider._user = "usr"
|
||||
provider._agent = "hermes"
|
||||
uri = "viking://user/peers/hermes/memories/preferences/mem_abc123.md"
|
||||
spawned = []
|
||||
|
||||
class StubThread:
|
||||
def __init__(self, *args, **kwargs):
|
||||
spawned.append((args, kwargs))
|
||||
|
||||
def start(self):
|
||||
raise AssertionError("non-URI remove should not spawn a mirror thread")
|
||||
|
||||
import plugins.memory.openviking as _mod
|
||||
monkeypatch.setattr(_mod.threading, "Thread", StubThread)
|
||||
|
||||
provider.on_memory_write(
|
||||
action,
|
||||
"memory",
|
||||
content,
|
||||
metadata={"uri": uri, "old_text": "stale fact"},
|
||||
)
|
||||
|
||||
assert spawned == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prefetch staleness: a prefetch worker that finishes AFTER a session switch
|
||||
# must drop its result instead of repopulating the new session with stale
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue