{"repo":"NousResearch/hermes-agent","public_id":"2591","display_label":"Adding missing /resume command handler to interactive CLI","summary":"Multiple pull requests and a bug report addressing the same gap: the interactive CLI advertised a /resume command but had no handler implemented for it. Members propose various implementations of that missing handler, with some also touching session log truncation and a reopen_session API.","subsystem":"hermes_cli","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":3315},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-03-27T02:04:29+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":2591,"pr_number":3315,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":3123,"pr_number":3315,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":2591,"title":"[Bug]: interactive CLI advertises /resume but does not handle it","url":null,"state":"closed"},{"kind":"pr","number":2592,"title":"fix(cli): implement /resume command handler for interactive CLI","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2893","number":2893},{"kind":"pr","value":"#3183","number":3183}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":2593,"title":"fix(cli): add interactive /resume handler","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"1519c4d477a19bcd426af8b6a70c6cbc6f4edeb4","number":0},{"kind":"pr","value":"#3315","number":3315}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":2597,"title":"fix(cli): implement missing resume command handler in interactive CLI","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2593","number":2593},{"kind":"pr","value":"#2592","number":2592}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":2722,"title":"fix(cli): implement /resume command for the interactive CLI","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"bd01ec788","number":0},{"kind":"commit","value":"f24956ba1","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":3315,"title":"fix(session): add /resume CLI handler, session log truncation guard, reopen_session API","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_state.py","run_agent.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -2949,6 +2949,82 @@ def new_session(self, silent=False):\n         if not silent:\n             print(\"(^_^)v New session started!\")\n \n+    def _handle_resume_command(self, cmd_original: str) -> None:\n+        \"\"\"Handle /resume <session_id_or_title> — switch to a previous session mid-conversation.\"\"\"\n+        parts = cmd_original.split(None, 1)\n+        target = parts[1].strip() if len(parts) > 1 else \"\"\n+\n+        if not target:\n+            _cprint(\"  Usage: /resume <session_id_or_title>\")\n+            _cprint(\"  Tip:   Use /history or `hermes sessions list` to find sessions.\")\n+            return\n+\n+        if not self._session_db:\n+            _cprint(\"  Session database not available.\")\n+            return\n+\n+        # Resolve title or ID\n+        from hermes_cli.main import _resolve_session_by_name_or_id\n+        resolved = _resolve_session_by_name_or_id(target)\n+        target_id = resolved or target\n+\n+        session_meta = self._session_db.get_session(target_id)\n+        if not session_meta:\n+            _cprint(f\"  Session not found: {target}\")\n+            _cprint(\"  Use /history or `hermes sessions list` to see available sessions.\")\n+            return\n+\n+        if target_id == self.session_id:\n+            _cprint(\"  Already on that session.\")\n+            return\n+\n+        # End current session\n+        try:\n+            self._session_db.end_session(self.session_id, \"resumed_other\")\n+        except Exception:\n+            pass\n+\n+        # Switch to the target session\n+        self.session_id = target_id\n+        self._resumed = True\n+        self._pending_title = None\n+\n+        # Load conversation history\n+        restored = self._session_db.get_messages_as_conversation(target_id)\n+        self.conversation_history = restored or []\n+\n+        # Re-open the target session so it's not marked as ended\n+        try:\n+            self._session_db.reopen_session(target_id)\n+        except Exception:\n+            pass\n+\n+        # Sync the agent if already initialised\n+        if self.agent:\n+            self.agent.session_id = target_id\n+            self.agent.reset_session_state()\n+            if hasattr(self.agent, \"_last_flushed_db_idx\"):\n+                self.agent._last_flushed_db_idx = len(self.conversation_history)\n+            if hasattr(self.agent, \"_todo_store\"):\n+                try:\n+                    from tools.todo_tool import TodoStore\n+                    self.agent._todo_store = TodoStore()\n+                except Exception:\n+                    pass\n+            if hasattr(self.agent, \"_invalidate_system_prompt\"):\n+                self.agent._invalidate_system_prompt()\n+\n+        title_part = f\" \\\"{session_meta['title']}\\\"\" if session_meta.get(\"title\") else \"\"\n+        msg_count = len([m for m in self.conversation_history if m.get(\"role\") == \"user\"])\n+        if self.conversation_history:\n+            _cprint(\n+                f\"  ↻ Resumed session {target_id}{title_part}\"\n+                f\" ({msg_count} user message{'s' if msg_count != 1 else ''},\"\n+                f\" {len(self.conversation_history)} total)\"\n+            )\n+        else:\n+            _cprint(f\"  ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.\")\n+\n     def reset_conversation(self):\n         \"\"\"Reset the conversation by starting a new session.\"\"\"\n         self.new_session()","@@ -3667,6 +3743,8 @@ def process_command(self, command: str) -> bool:\n                     _cprint(\"  Session database not available.\")\n         elif canonical == \"new\":\n             self.new_session()\n+        elif canonical == \"resume\":\n+            self._handle_resume_command(cmd_original)\n         elif canonical == \"provider\":\n             self._show_model_and_providers()\n         elif canonical == \"prompt\":"]},{"file":"hermes_state.py","hunks":["@@ -284,6 +284,15 @@ def end_session(self, session_id: str, end_reason: str) -> None:\n             )\n             self._conn.commit()\n \n+    def reopen_session(self, session_id: str) -> None:\n+        \"\"\"Clear ended_at/end_reason so a session can be resumed.\"\"\"\n+        with self._lock:\n+            self._conn.execute(\n+                \"UPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?\",\n+                (session_id,),\n+            )\n+            self._conn.commit()\n+\n     def update_system_prompt(self, session_id: str, system_prompt: str) -> None:\n         \"\"\"Store the full assembled system prompt snapshot.\"\"\"\n         with self._lock:"]},{"file":"run_agent.py","hunks":["@@ -2055,6 +2055,23 @@ def _save_session_log(self, messages: List[Dict[str, Any]] = None):\n                     msg[\"content\"] = self._clean_session_content(msg[\"content\"])\n                 cleaned.append(msg)\n \n+            # Guard: never overwrite a larger session log with fewer messages.\n+            # This protects against data loss when --resume loads a session whose\n+            # messages weren't fully written to SQLite — the resumed agent starts\n+            # with partial history and would otherwise clobber the full JSON log.\n+            if self.session_log_file.exists():\n+                try:\n+                    existing = json.loads(self.session_log_file.read_text(encoding=\"utf-8\"))\n+                    existing_count = existing.get(\"message_count\", len(existing.get(\"messages\", [])))\n+                    if existing_count > len(cleaned):\n+                        logging.debug(\n+                            \"Skipping session log overwrite: existing has %d messages, current has %d\",\n+                            existing_count, len(cleaned),\n+                        )\n+                        return\n+                except Exception:\n+                    pass  # corrupted existing file — allow the overwrite\n+\n             entry = {\n                 \"session_id\": self.session_id,\n                 \"model\": self.model,\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}