Memory tool parameter naming and schema validation bugs
NousResearch/hermes-agent · cluster 15843 · agent
Data as of
5 open items. Fix merged in #21238. Check it covers your case.
The short answer
- Known?
- Yes. 29 related items grouped here, 7 issues already closed.
- Fix event
- A fix was merged to main in PR #21238. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #21238, #49997. That is separate from the fix event. - Still open
- 3 issues and 2 PRs.
Next step: read #21238, then check whether your report is one of the 3 still open.
Read #21238Pick up a Lab taskMost members concern mismatches between parameter names (old_text vs old_string), missing schema constraints for replace/remove actions, and broken memory tool operations due to unrecognized parameters. A smaller cluster addresses send_message schema fields not matching runtime enforcement.
Open issues3
- Issue#20735Inconsistent parameter naming: `old_text` vs `old_string` across tools
- Issue#55095[Bug] Holographic memory plugin: on_memory_write only mirrors "add", silently drops "replace" and "remove"
- Issue#59772[API Consistency] memory, patch, and skill_manage use different field names for the same semantic operation
Open PRs2
Fixes on record
-
#21238 fix(memory): require schema fields by action (salvage #19180)
files:
tests/tools/test_memory_tool_schema.py,tools/memory_tool.pyShow the diff
-
tests/tools/test_memory_tool_schema.py@@ -0,0 +1,39 @@ +import json +from tools.memory_tool import MEMORY_SCHEMA + + +def test_memory_schema_requires_content_and_old_text_for_replace_action(): + schema = MEMORY_SCHEMA["parameters"] + assert schema["required"] == ["action", "target"] + + all_of = schema.get("allOf") + assert all_of, "memory schema should use conditional requirements" + + replace_requirements = [ + branch["then"].get("required", []) + for branch in all_of + if branch.get("if", {}).get("properties", {}).get("action", {}).get("const") == "replace" + ] + assert replace_requirements == [["old_text", "content"]] + + +def test_memory_schema_requires_content_for_add_action(): + add_requirements = [ + branch["then"].get("required", []) + for branch in MEMORY_SCHEMA["parameters"].get("allOf", []) + if branch.get("if", {}).get("properties", {}).get("action", {}).get("const") == "add" + ] + assert add_requirements == [["content"]] + + +def test_memory_schema_requires_old_text_for_remove_action(): + remove_requirements = [ + branch["then"].get("required", []) + for branch in MEMORY_SCHEMA["parameters"].get("allOf", []) + if branch.get("if", {}).get("properties", {}).get("action", {}).get("const") == "remove" + ] + assert remove_requirements == [["old_text"]] + + +def test_memory_schema_is_json_serializable(): + json.dumps(MEMORY_SCHEMA) -
tools/memory_tool.py@@ -560,6 +560,29 @@ def check_memory_requirements() -> bool: }, }, "required": ["action", "target"], + "allOf": [ + { + "if": { + "properties": {"action": {"const": "add"}}, + "required": ["action"], + }, + "then": {"required": ["content"]}, + }, + { + "if": { + "properties": {"action": {"const": "replace"}}, + "required": ["action"], + }, + "then": {"required": ["old_text", "content"]}, + }, + { + "if": { + "properties": {"action": {"const": "remove"}}, + "required": ["action"], + }, + "then": {"required": ["old_text"]}, + }, + ], }, }
-
-
#49997 fix(memory): recover from missing old_text on single-op replace/remove
files:
tests/tools/test_memory_tool.py,tools/memory_tool.pyShow the diff
-
tests/tools/test_memory_tool.py@@ -435,12 +435,33 @@ def test_add_via_tool(self, store): assert result["success"] is True def test_replace_requires_old_text(self, store): + # Missing old_text on a single-op replace is recoverable, not a dead-end: + # return the current inventory + a retry instruction so the model can + # reissue with old_text set. (issues #43412, #49466) + store.add("memory", "fact A") + store.add("memory", "fact B") result = json.loads(memory_tool(action="replace", content="new", store=store)) assert result["success"] is False + assert "old_text" in result["error"] + assert result["current_entries"] == ["fact A", "fact B"] + assert "usage" in result def test_remove_requires_old_text(self, store): + store.add("memory", "fact A") result = json.loads(memory_tool(action="remove", store=store)) assert result["success"] is False + assert "old_text" in result["error"] + assert result["current_entries"] == ["fact A"] + assert "usage" in result + + def test_replace_missing_content_still_distinct_error(self, store): + # When old_text IS present but content is missing, keep the original + # content-specific error (don't route through the old_text recovery path). + store.add("memory", "fact A") + result = json.loads(memory_tool(action="replace", old_text="fact A", store=store)) + assert result["success"] is False + assert "content is required" in result["error"] + assert "current_entries" not in result class TestMemoryBatch: -
tools/memory_tool.py@@ -835,6 +835,38 @@ def _apply_batch_write_gate(target: str, operations: List[Dict[str, Any]]) -> Op ) +def _missing_old_text_error(store: "MemoryStore", target: str, action: str) -> str: + """Build a recoverable error for a replace/remove call that arrived without + ``old_text``. + + ``replace``/``remove`` are inherently targeted -- without ``old_text`` there + is no entry to act on, so we cannot fulfil the call. But returning a bare + "old_text is required" is a dead-end: some structured-output clients omit the + optional ``old_text`` field (it isn't, and can't be, schema-required without + a top-level combinator the Codex backend rejects -- see + tests/tools/test_memory_tool_schema.py). So instead we return the current + entry inventory plus an explicit retry instruction, letting the model reissue + the call with ``old_text`` set to a unique substring of the entry it means. + Mirrors the batch path's ``_batch_error`` shape. (issues #43412, #49466) + """ + entries = store._entries_for(target) + current = store._char_count(target) + limit = store._char_limit(target) + return json.dumps( + { + "success": False, + "error": ( + f"'{action}' needs old_text -- a short unique substring of the entry " + f"to {action}. None was provided. Reissue the {action} with old_text " + f"set to part of one of the current_entries below." + ), + "current_entries": entries, + "usage": f"{current:,}/{limit:,}", + }, + ensure_ascii=False, + ) + + def memory_tool( action: str = None, target: str = "memory", @@ -876,9 +908,15 @@ def memory_tool( return tool_error("Content is required for 'add' action.", success=False) if action == "replace" and (not old_text or not content): missing = "old_text" if not old_text else "content" + if not old_text: + # The client/model omitted old_text. Replace is inherently targeted + # -- we can't guess which entry. Return the current inventory plus a + # retry instruction so the model can reissue with old_text set, + # instead of hitting a dead-end error. (issues #43412, #49466) + return _missing_old_text_error(store, target, "replace") return tool_error(f"{missing} is required for 'replace' action.", success=False) if action == "remove" and not old_text: - return tool_error("old_text is required for 'remove' action.", success=False) + return _missing_old_text_error(store, target, "remove") # Approval gate: when on, stages the write (background/gateway) or prompts # inline (interactive CLI); when off (default) passes straight through. @@ -971,7 +1009,7 @@ def apply_memory_pending(payload: Dict[str, Any], store: "MemoryStore") -> Dict[ }, "old_text": { "type": "string", - "description": "Short unique substring identifying the entry to replace or remove (single-op shape)." + "description": "REQUIRED for 'replace' and 'remove' (single-op shape): a short unique substring identifying the existing entry to modify. Omit only for 'add'." }, "operations": { "type": "array",
-
15 PRs closed without merging
From the close comments: done elsewhere 3 · duplicate 3 · superseded by another PR 2 · won't fix / by design 1 · other 6
-
#15859 fix(memory): accept common LLM aliases for action / new_text · other →
#15843 - #15957 fix(tools): align memory tool handler parameter name with schema (old_text → old_string) · other
-
#19180 fix: require memory schema fields by action · duplicate →
#12604,#15957 - #21450 fix(memory): accept old_string as old_text alias · other
-
#21875 fix(memory): normalize replace/remove alias forwarding · other →
#21844
Show 10 more
- #21896 fix(tools): resolve old_string/new_text parameter aliases in memory tool · won't fix / by design
-
#23399 fix(memory): add if/then schema constraint requiring old_text for rep… · done elsewhere →
c6bf6bd,5a3e5b2 -
#32381 fix(tools): validate send_message send args in schema (#32376) · done elsewhere →
c6c8abb -
#32385 fix(send_message): declare required params in schema so model-side validator catches malformed calls · other →
#32381,#32376,#21311,#21316 -
#32435 fix(tools): send_message schema required fields match runtime enforcement · superseded by another PR →
#32376 -
#32577 fix(memory): accept old_string as alias for old_text in memory tool · other →
4cd1790 -
#43426 fix(memory): preserve old_text in memory tool schema (#43412) · done elsewhere →
c6bf6bd,#49997,#43412 -
#43484 fix(tools): add old_text to memory tool required schema fields (#43412) · duplicate →
#43426,#43412 -
#43526 fix(memory): add old_text to schema required list (#43412) · superseded by another PR →
#43426,#43412 -
#56274 fix(memory): handle replace and remove in holographic on_memory_write · duplicate →
#55098
Closed issues · 7
- Closed#15843[Bug]: Memory Tool Cannot Delete Old Data - memory tool remove/replace operations failing
- Closed#19171memory tool: replace action schema missing old_text/content as required
- Closed#21844Bug: memory tool replace/remove operations fail — old_text parameter not recognized
- Closed#23381memory tool replace/remove fails with "old_text is required" even when old_text is provided
- Closed#32376[Bug]: send_message schema declares `required: []` but handler enforces `target`/`message` at runtime
- Closed#43412[bug] memory tool remove action rejects valid old_text arg with "old_text is required" error
- Closed#49466bug: memory remove and replace actions broken in v0.17.0
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.07: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab