# NousResearch/hermes-agent · Adding missing /resume command handler to interactive CLI

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #3315. Verify it addresses your case.
- **Recommended:** read PR #3315 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #2591: \[Bug\]: interactive CLI advertises /resume but does not handle it

## Prior & competing PRs

- **#2592: fix(cli): implement /resume command handler for interactive CLI**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#2893`, `#3183`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#2597: fix(cli): implement missing resume command handler in interactive CLI**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#2593`, `#2592`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#2722: fix(cli): implement /resume command for the interactive CLI**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `bd01ec788`, `f24956ba1`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#3315: fix(session): add /resume CLI handler, session log truncation guard, reopen_session API**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `hermes_state.py`, `run_agent.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -2949,6 +2949,82 @@ def new_session(self, silent=False):
             if not silent:
                 print("(^_^)v New session started!")
     
    +    def _handle_resume_command(self, cmd_original: str) -> None:
    +        """Handle /resume <session_id_or_title> — switch to a previous session mid-conversation."""
    +        parts = cmd_original.split(None, 1)
    +        target = parts[1].strip() if len(parts) > 1 else ""
    +
    +        if not target:
    +            _cprint("  Usage: /resume <session_id_or_title>")
    +            _cprint("  Tip:   Use /history or `hermes sessions list` to find sessions.")
    +            return
    +
    +        if not self._session_db:
    +            _cprint("  Session database not available.")
    +            return
    +
    +        # Resolve title or ID
    +        from hermes_cli.main import _resolve_session_by_name_or_id
    +        resolved = _resolve_session_by_name_or_id(target)
    +        target_id = resolved or target
    +
    +        session_meta = self._session_db.get_session(target_id)
    +        if not session_meta:
    +            _cprint(f"  Session not found: {target}")
    +            _cprint("  Use /history or `hermes sessions list` to see available sessions.")
    +            return
    +
    +        if target_id == self.session_id:
    +            _cprint("  Already on that session.")
    +            return
    +
    +        # End current session
    +        try:
    +            self._session_db.end_session(self.session_id, "resumed_other")
    +        except Exception:
    +            pass
    +
    +        # Switch to the target session
    +        self.session_id = target_id
    +        self._resumed = True
    +        self._pending_title = None
    +
    +        # Load conversation history
    +        restored = self._session_db.get_messages_as_conversation(target_id)
    +        self.conversation_history = restored or []
    +
    +        # Re-open the target session so it's not marked as ended
    +        try:
    +            self._session_db.reopen_session(target_id)
    +        except Exception:
    +            pass
    +
    +        # Sync the agent if already initialised
    +        if self.agent:
    +            self.agent.session_id = target_id
    +            self.agent.reset_session_state()
    +            if hasattr(self.agent, "_last_flushed_db_idx"):
    +                self.agent._last_flushed_db_idx = len(self.conversation_history)
    +            if hasattr(self.agent, "_todo_store"):
    +                try:
    +                    from tools.todo_tool import TodoStore
    +                    self.agent._todo_store = TodoStore()
    +                except Exception:
    +                    pass
    +            if hasattr(self.agent, "_invalidate_system_prompt"):
    +                self.agent._invalidate_system_prompt()
    +
    +        title_part = f" \"{session_meta['title']}\"" if session_meta.get("title") else ""
    +        msg_count = len([m for m in self.conversation_history if m.get("role") == "user"])
    +        if self.conversation_history:
    +            _cprint(
    +                f"  ↻ Resumed session {target_id}{title_part}"
    +                f" ({msg_count} user message{'s' if msg_count != 1 else ''},"
    +                f" {len(self.conversation_history)} total)"
    +            )
    +        else:
    +            _cprint(f"  ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.")
    +
         def reset_conversation(self):
             """Reset the conversation by starting a new session."""
             self.new_session()
    ```

    ```diff
    @@ -3667,6 +3743,8 @@ def process_command(self, command: str) -> bool:
                         _cprint("  Session database not available.")
             elif canonical == "new":
                 self.new_session()
    +        elif canonical == "resume":
    +            self._handle_resume_command(cmd_original)
             elif canonical == "provider":
                 self._show_model_and_providers()
             elif canonical == "prompt":
    ```
  - `hermes_state.py`

    ```diff
    @@ -284,6 +284,15 @@ def end_session(self, session_id: str, end_reason: str) -> None:
                 )
                 self._conn.commit()
     
    +    def reopen_session(self, session_id: str) -> None:
    +        """Clear ended_at/end_reason so a session can be resumed."""
    +        with self._lock:
    +            self._conn.execute(
    +                "UPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?",
    +                (session_id,),
    +            )
    +            self._conn.commit()
    +
         def update_system_prompt(self, session_id: str, system_prompt: str) -> None:
             """Store the full assembled system prompt snapshot."""
             with self._lock:
    ```
  - `run_agent.py`

    ```diff
    @@ -2055,6 +2055,23 @@ def _save_session_log(self, messages: List[Dict[str, Any]] = None):
                         msg["content"] = self._clean_session_content(msg["content"])
                     cleaned.append(msg)
     
    +            # Guard: never overwrite a larger session log with fewer messages.
    +            # This protects against data loss when --resume loads a session whose
    +            # messages weren't fully written to SQLite — the resumed agent starts
    +            # with partial history and would otherwise clobber the full JSON log.
    +            if self.session_log_file.exists():
    +                try:
    +                    existing = json.loads(self.session_log_file.read_text(encoding="utf-8"))
    +                    existing_count = existing.get("message_count", len(existing.get("messages", [])))
    +                    if existing_count > len(cleaned):
    +                        logging.debug(
    +                            "Skipping session log overwrite: existing has %d messages, current has %d",
    +                            existing_count, len(cleaned),
    +                        )
    +                        return
    +                except Exception:
    +                    pass  # corrupted existing file — allow the overwrite
    +
                 entry = {
                     "session_id": self.session_id,
                     "model": self.model,
    ```
- **#2593: fix(cli): add interactive /resume handler**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1519c4d477a19bcd426af8b6a70c6cbc6f4edeb4`, `#3315`

## Fix status

- Merged to main in PR #3315 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-27`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `hermes_cli`
