{"repo":"NousResearch/hermes-agent","public_id":"1412","display_label":"Session delete and ID lookup bug corrections","summary":"Multiple bugs around deleting sessions by ID were addressed, including missing owner/profile context passed to endpoints and session ID not being looked up before delete operations. Related changes also cover session listing and MCP schema normalization.","subsystem":"apps/desktop","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":2102},"resolution":{"state":"partially_resolved","by":"pr-2102"},"freshness":{"state":"stale","last_verified_at":"2026-03-20T02:06:56+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.22476190476190475},"fix_edges":[{"issue_number":2068,"pr_number":2102,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":1412,"title":"[Bug]: Cannot delete sessions by ID","url":null,"state":"closed"},{"kind":"issue","number":2068,"title":"[Bug]: Session ID Truncated in `hermes sessions list`","url":null,"state":"closed"},{"kind":"issue","number":44117,"title":"[Bug]: Cannot delete sessions in default proffile / ID not found","url":null,"state":"closed"},{"kind":"pr","number":2102,"title":"fix(tools,cli): normalise MCP schemas + expand session list columns","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","tests/tools/test_mcp_tool.py","tools/mcp_tool.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -3721,20 +3721,20 @@ def cmd_sessions(args):\n                 return\n             has_titles = any(s.get(\"title\") for s in sessions)\n             if has_titles:\n-                print(f\"{'Title':<22} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n-                print(\"─\" * 100)\n+                print(f\"{'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n+                print(\"─\" * 110)\n             else:\n                 print(f\"{'Preview':<50} {'Last Active':<13} {'Src':<6} {'ID'}\")\n-                print(\"─\" * 90)\n+                print(\"─\" * 95)\n             for s in sessions:\n                 last_active = _relative_time(s.get(\"last_active\"))\n                 preview = s.get(\"preview\", \"\")[:38] if has_titles else s.get(\"preview\", \"\")[:48]\n                 if has_titles:\n-                    title = (s.get(\"title\") or \"—\")[:20]\n-                    sid = s[\"id\"][:20]\n-                    print(f\"{title:<22} {preview:<40} {last_active:<13} {sid}\")\n+                    title = (s.get(\"title\") or \"—\")[:30]\n+                    sid = s[\"id\"]\n+                    print(f\"{title:<32} {preview:<40} {last_active:<13} {sid}\")\n                 else:\n-                    sid = s[\"id\"][:20]\n+                    sid = s[\"id\"]\n                     print(f\"{preview:<50} {last_active:<13} {s['source']:<6} {sid}\")\n \n         elif action == \"export\":"]},{"file":"tests/tools/test_mcp_tool.py","hunks":["@@ -106,6 +106,18 @@ def test_empty_input_schema_gets_default(self):\n         assert schema[\"parameters\"][\"type\"] == \"object\"\n         assert schema[\"parameters\"][\"properties\"] == {}\n \n+    def test_object_schema_without_properties_gets_normalized(self):\n+        from tools.mcp_tool import _convert_mcp_schema\n+\n+        mcp_tool = _make_mcp_tool(\n+            name=\"ask\",\n+            description=\"Ask Crawl4AI\",\n+            input_schema={\"type\": \"object\"},\n+        )\n+        schema = _convert_mcp_schema(\"crawl4ai\", mcp_tool)\n+\n+        assert schema[\"parameters\"] == {\"type\": \"object\", \"properties\": {}}\n+\n     def test_tool_name_prefix_format(self):\n         from tools.mcp_tool import _convert_mcp_schema\n ","@@ -1893,6 +1905,33 @@ def test_system_prompt_prepended(self):\n         messages = call_args.kwargs[\"messages\"]\n         assert messages[0] == {\"role\": \"system\", \"content\": \"Be helpful\"}\n \n+    def test_server_tools_with_object_schema_are_normalized(self):\n+        \"\"\"Server-provided tools should gain empty properties for object schemas.\"\"\"\n+        fake_client = MagicMock()\n+        fake_client.chat.completions.create.return_value = _make_llm_response()\n+        server_tool = SimpleNamespace(\n+            name=\"ask\",\n+            description=\"Ask Crawl4AI\",\n+            inputSchema={\"type\": \"object\"},\n+        )\n+\n+        with patch(\n+            \"agent.auxiliary_client.call_llm\",\n+            return_value=fake_client.chat.completions.create.return_value,\n+        ) as mock_call:\n+            params = _make_sampling_params(tools=[server_tool])\n+            asyncio.run(self.handler(None, params))\n+\n+        tools = mock_call.call_args.kwargs[\"tools\"]\n+        assert tools == [{\n+            \"type\": \"function\",\n+            \"function\": {\n+                \"name\": \"ask\",\n+                \"description\": \"Ask Crawl4AI\",\n+                \"parameters\": {\"type\": \"object\", \"properties\": {}},\n+            },\n+        }]\n+\n     def test_length_stop_reason(self):\n         \"\"\"finish_reason='length' maps to stopReason='maxTokens'.\"\"\"\n         fake_client = MagicMock()"]},{"file":"tools/mcp_tool.py","hunks":["@@ -605,7 +605,9 @@ async def __call__(self, context, params):\n                     \"function\": {\n                         \"name\": getattr(t, \"name\", \"\"),\n                         \"description\": getattr(t, \"description\", \"\") or \"\",\n-                        \"parameters\": getattr(t, \"inputSchema\", {}) or {},\n+                        \"parameters\": _normalize_mcp_input_schema(\n+                            getattr(t, \"inputSchema\", None)\n+                        ),\n                     },\n                 }\n                 for t in server_tools","@@ -1213,6 +1215,17 @@ def _check() -> bool:\n # Discovery & registration\n # ---------------------------------------------------------------------------\n \n+def _normalize_mcp_input_schema(schema: dict | None) -> dict:\n+    \"\"\"Normalize MCP input schemas for LLM tool-calling compatibility.\"\"\"\n+    if not schema:\n+        return {\"type\": \"object\", \"properties\": {}}\n+\n+    if schema.get(\"type\") == \"object\" and \"properties\" not in schema:\n+        return {**schema, \"properties\": {}}\n+\n+    return schema\n+\n+\n def _convert_mcp_schema(server_name: str, mcp_tool) -> dict:\n     \"\"\"Convert an MCP tool listing to the Hermes registry schema format.\n ","@@ -1231,10 +1244,7 @@ def _convert_mcp_schema(server_name: str, mcp_tool) -> dict:\n     return {\n         \"name\": prefixed_name,\n         \"description\": mcp_tool.description or f\"MCP tool {mcp_tool.name} from {server_name}\",\n-        \"parameters\": mcp_tool.inputSchema if mcp_tool.inputSchema else {\n-            \"type\": \"object\",\n-            \"properties\": {},\n-        },\n+        \"parameters\": _normalize_mcp_input_schema(mcp_tool.inputSchema),\n     }\n \n \n"]}]},{"kind":"pr","number":44138,"title":"fix(desktop): pass owning profile to server on session delete/archive","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"18f429a89b","number":0}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":44273,"title":"fix(cli): resolve session ID before delete like GET/PATCH do (#44117)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":83968,"title":"fix(desktop): pass session owner to archive and delete endpoints","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}