NousResearch/hermes-agent

Session resume bugs and CLI fixes

Cluster 3123 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #3315 on 27 Mar 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #3315. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #3315. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple bug reports and fix attempts address problems with resuming previous sessions, including a /resume CLI command, output redirection, data loss, and startup hangs. Work spans CLI, TUI, and session storage layers.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #3315 fix(session): add /resume CLI handler, session log truncation guard, reopen_session API

    merged to main

    files: cli.py, hermes_state.py, run_agent.py

    Show the diff
    • cli.py
      @@ -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()
      @@ -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
      @@ -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
      @@ -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,

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #3183 fix: preserve session_db on UNIQUE conflict and add /resume CLI command · done elsewhere → f24956b, bd01ec7, a456b50
  • #3225 fix(session): prevent resume data loss and add /resume slash command · merged via another change → #3315
Closed issues · 1
  • Closed#3123[Bug]: Session --resume not working
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item 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.

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

At a glance

Subsystem
hermes cli
Merged PRs
#3315
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
27 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.