# NousResearch/hermes-agent · Prevent empty ghost sessions cluttering resume list

## Open now

- Nothing open on GitHub.

These issues and PRs address TUI and CLI behavior that eagerly writes empty sessions to the database, causing ghost rows to appear in /resume. Fixes involve lazy session creation, dropping empty sessions on exit, and ensuring sessions are properly ended on finalize.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #18370. Verify it addresses your case.
- **Recommended:** read PR #18370 (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: `resolved` · deciding PR(s): `pr-18370`. 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.50`: 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

- #18269: \[Bug\]: TUI eagerly persists empty sessions to DB — ghost sessions clutter /resume

## Prior & competing PRs

- **#18370: fix: lazy session creation — defer DB row until first message**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/main.py`, `hermes_state.py`, `run_agent.py`, `tests/run_agent/test_860_dedup.py`, `tui_gateway/server.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -934,6 +934,20 @@ def _run_state_db_auto_maintenance(session_db) -> None:
         try:
             from hermes_cli.config import load_config as _load_full_config
             from hermes_constants import get_hermes_home as _get_hermes_home
    +        _hermes_home_maint = _get_hermes_home()
    +
    +        # One-time prune of empty TUI ghost sessions.
    +        try:
    +            if not session_db.get_meta("ghost_session_prune_v1"):
    +                pruned = session_db.prune_empty_ghost_sessions(
    +                    sessions_dir=_hermes_home_maint / "sessions"
    +                )
    +                session_db.set_meta("ghost_session_prune_v1", "1")
    +                if pruned:
    +                    logger.info("Pruned %d empty TUI ghost sessions", pruned)
    +        except Exception as _prune_exc:
    +            logger.debug("Ghost session prune skipped: %s", _prune_exc)
    +
             cfg = (_load_full_config().get("sessions") or {})
             if not cfg.get("auto_prune", False):
                 return
    ```

    ```diff
    @@ -941,7 +955,7 @@ def _run_state_db_auto_maintenance(session_db) -> None:
                 retention_days=int(cfg.get("retention_days", 90)),
                 min_interval_hours=int(cfg.get("min_interval_hours", 24)),
                 vacuum=bool(cfg.get("vacuum_after_prune", True)),
    -            sessions_dir=_get_hermes_home() / "sessions",
    +            sessions_dir=_hermes_home_maint / "sessions",
             )
         except Exception as exc:
             logger.debug("state.db auto-maintenance skipped: %s", exc)
    ```

    ```diff
    @@ -3618,14 +3632,18 @@ def _init_agent(self, *, model_override: str = None, runtime_override: dict = No
                     tuple(runtime.get("args") or ()),
                 )
     
    -            if self._pending_title and self._session_db:
    +            # Force-create DB row on /title intent, then apply title.
    +            if self._pending_title and self._session_db and self.agent:
                     try:
    -                    self._session_db.set_session_title(self.session_id, self._pending_title)
    -                    _cprint(f"  Session title applied: {self._pending_title}")
    -                    self._pending_title = None
    +                    self.agent._ensure_db_session()
    +                    if self.agent._session_db_created:
    +                        self._session_db.set_session_title(self.session_id, self._pending_title)
    +                        _cprint(f"  Session title applied: {self._pending_title}")
    +                        self._pending_title = None
    +                    # else: row creation failed transiently — keep _pending_title for retry
                     except (ValueError, Exception) as e:
                         _cprint(f"  Could not apply pending title: {e}")
    -                    self._pending_title = None
    +                    # Keep _pending_title so it can be retried after row creation succeeds
                 return True
             except Exception as e:
                 ChatConsole().print(f"[bold red]Failed to initialize agent: {e}[/]")
    ```

    ```diff
    @@ -4953,6 +4971,7 @@ def new_session(self, silent=False):
     
                 if self._session_db:
                     try:
    +                    self.agent._session_db_created = False
                         self._session_db.create_session(
                             session_id=self.session_id,
                             source=os.environ.get("HERMES_SESSION_SOURCE", "cli"),
    ```

    ```diff
    @@ -4962,6 +4981,7 @@ def new_session(self, silent=False):
                                 "reasoning_config": self.reasoning_config,
                             },
                         )
    +                    self.agent._session_db_created = True
                     except Exception:
                         pass
                 # Notify memory providers that session_id rotated to a fresh
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -800,6 +800,8 @@ def _print_tui_exit_summary(session_id: Optional[str], active_session_file: Opti
     
             title = db.get_session_title(target)
             message_count = int(session.get("message_count") or 0)
    +        if message_count == 0:
    +            return  # No real conversation — don't show resume info
             input_tokens = [redacted:secret-assignment]"input_tokens") or 0)
             output_tokens = [redacted:secret-assignment]"output_tokens") or 0)
             cache_read_tokens = [redacted:secret-assignment]"cache_read_tokens") or 0)
    ```
  - `hermes_state.py`

    ```diff
    @@ -514,7 +514,7 @@ def _init_schema(self):
         # Session lifecycle
         # =========================================================================
     
    -    def create_session(
    +    def _insert_session_row(
             self,
             session_id: str,
             source: str,
    ```

    ```diff
    @@ -523,8 +523,8 @@ def create_session(
             system_prompt: str = None,
             user_id: str = None,
             parent_session_id: str = None,
    -    ) -> str:
    -        """Create a new session record. Returns the session_id."""
    +    ) -> None:
    +        """Shared INSERT OR IGNORE for session rows."""
             def _do(conn):
                 conn.execute(
                     """INSERT OR IGNORE INTO sessions (id, source, user_id, model, model_config,
    ```

    ```diff
    @@ -542,8 +542,11 @@ def _do(conn):
                     ),
                 )
             self._execute_write(_do)
    -        return session_id
     
    +    def create_session(self, session_id: str, source: str, **kwargs) -> str:
    +        """Create a new session record. Returns the session_id."""
    +        self._insert_session_row(session_id, source, **kwargs)
    +        return session_id
         def end_session(self, session_id: str, end_reason: str) -> None:
             """Mark a session as ended.
     
    ```

    ```diff
    @@ -679,21 +682,41 @@ def ensure_session(
             session_id: str,
             source: str = "unknown",
             model: str = None,
    -    ) -> None:
    -        """Ensure a session row exists, creating it with minimal metadata if absent.
    +        **kwargs,
    +    ) -> str:
    +        """Ensure a session row exists (INSERT OR IGNORE). Accepts optional kwargs."""
    +        self._insert_session_row(session_id, source, model=model, **kwargs)
    +        return session_id
    +
    +    def prune_empty_ghost_sessions(self, sessions_dir: "Optional[Path]" = None) -> int:
    +        """Remove empty TUI ghost sessions (no messages, no title, >24hr old)."""
    +        cutoff = time.time() - 86400  # Only sessions older than 24 hours
     
    -        Used by _flush_messages_to_session_db to recover from a failed
    -        create_session() call (e.g. transient SQLite lock at agent startup).
    -        INSERT OR IGNORE is safe to call even when the row already exists.
    -        """
             def _do(conn):
    -            conn.execute(
    -                """INSERT OR IGNORE INTO sessions
    -                   (id, source, model, started_at)
    -                   VALUES (?, ?, ?, ?)""",
    -                (session_id, source, model, time.time()),
    -            )
    -        self._execute_write(_do)
    +            rows = conn.execute("""
    +                SELECT id FROM sessions
    +                WHERE source = 'tui'
    +                  AND title IS NULL
    +                  AND ended_at IS NOT NULL
    +                  AND started_at < ?
    +                  AND NOT EXISTS (
    +                      SELECT 1 FROM messages WHERE messages.session_id = sessions.id
    +                  )
    +            """, (cutoff,)).fetchall()
    +            ids = [r[0] if isinstance(r, (tuple, list)) else r["id"] for r in rows]
    +            if ids:
    +                placeholders = ",".join("?" * len(ids))
    +                conn.execute(
    +                    f"DELETE FROM sessions WHERE id IN ({placeholders})", ids
    +                )
    +            return ids
    +
    +        removed_ids = self._execute_write(_do) or []
    +        # Clean up any on-disk session files (belt-and-suspenders)
    +        if sessions_dir and removed_ids:
    +            for sid in removed_ids:
    +                self._remove_session_files(sessions_dir, sid)
    +        return len(removed_ids)
     
         def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
             """Get a session by ID."""
    ```
  - `run_agent.py`

    ```diff
    @@ -1632,30 +1632,12 @@ def __init__(
             self._session_db = session_db
             self._parent_session_id = parent_session_id
             self._last_flushed_db_idx = 0  # tracks DB-write cursor to prevent duplicate writes
    -        if self._session_db:
    -            try:
    -                self._session_db.create_session(
    -                    session_id=self.session_id,
    -                    source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
    -                    model=self.model,
    -                    model_config={
    -                        "max_iterations": self.max_iterations,
    -                        "reasoning_config": reasoning_config,
    -                        "max_tokens": [redacted:secret-assignment]
    -                    },
    -                    user_id=None,
    -                    parent_session_id=self._parent_session_id,
    -                )
    -            except Exception as e:
    -                # Transient SQLite lock contention (e.g. CLI and gateway writing
    -                # concurrently) must NOT permanently disable session_search for
    -                # this agent.  Keep _session_db alive — subsequent message
    -                # flushes and session_search calls will still work once the
    -                # lock clears.  The session row may be missing from the index
    -                # for this run, but that is recoverable (flushes upsert rows).
    -                logger.warning(
    -                    "Session DB create_session failed (session_search still available): %s", e
    -                )
    +        self._session_db_created = False  # DB row deferred to run_conversation()
    +        self._session_init_model_config = {
    +            "max_iterations": self.max_iterations,
    +            "reasoning_config": reasoning_config,
    +            "max_tokens": [redacted:secret-assignment]
    +        }
             
             # In-memory todo list for task planning (one per agent/session)
             from tools.todo_tool import TodoStore
    ```

    ```diff
    @@ -2170,6 +2152,28 @@ def __init__(
                     "is_anthropic_oauth": self._is_anthropic_oauth,
                 })
     
    +    def _ensure_db_session(self) -> None:
    +        """Create session DB row on first use. Disables _session_db on failure."""
    +        if self._session_db_created or not self._session_db:
    +            return
    +        try:
    +            self._session_db.create_session(
    +                session_id=self.session_id,
    +                source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
    +                model=self.model,
    +                model_config=self._session_init_model_config,
    +                system_prompt=self._cached_system_prompt,
    +                user_id=None,
    +                parent_session_id=self._parent_session_id,
    +            )
    +            self._session_db_created = True
    +        except Exception as e:
    +            # Transient failure (e.g. SQLite lock). Keep _session_db alive —
    +            # _session_db_created stays False so next run_conversation() retries.
    +            logger.warning(
    +                "Session DB creation failed (will retry next turn): %s", e
    +            )
    +
         def reset_session_state(self):
             """Reset all session-scoped token counters to 0 for a fresh session.
             
    ```

    ```diff
    @@ -3719,14 +3723,9 @@ def _flush_messages_to_session_db(self, messages: List[Dict], conversation_histo
                 return
             self._apply_persist_user_message_override(messages)
             try:
    -            # If create_session() failed at startup (e.g. transient lock), the
    -            # session row may not exist yet.  ensure_session() uses INSERT OR
    -            # IGNORE so it is a no-op when the row is already there.
    -            self._session_db.ensure_session(
    -                self.session_id,
    -                source=self.platform or "cli",
    -                model=self.model,
    -            )
    +            # Retry row creation if the earlier attempt failed transiently.
    +            if not self._session_db_created:
    +                self._ensure_db_session()
                 start_idx = len(conversation_history) if conversation_history else 0
                 flush_from = max(start_idx, self._last_flushed_db_idx)
                 for msg in messages[flush_from:]:
    ```

    ```diff
    @@ -9056,12 +9055,15 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token
                     self.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
                     # Update session_log_file to point to the new session's JSON file
                     self.session_log_file = self.logs_dir / f"session_{self.session_id}.json"
    +                self._session_db_created = False
                     self._session_db.create_session(
                         session_id=self.session_id,
                         source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
                         model=self.model,
    +                    model_config=self._session_init_model_config,
                         parent_session_id=old_session_id,
                     )
    +                self._session_db_created = True
                     # Auto-number the title for the continuation session
                     if old_title:
                         try:
    ```

    ```diff
    @@ -10351,6 +10353,8 @@ def run_conversation(
             # Installed once, transparent when streams are healthy, prevents crash on write.
             _install_safe_stdio()
     
    +        self._ensure_db_session()
    +
             # Tag all log records on this thread with the session ID so
             # ``hermes logs --session <id>`` can filter a single conversation.
             from hermes_logging import set_session_context
    ```
  - `tests/run_agent/test_860_dedup.py`

    ```diff
    @@ -38,6 +38,8 @@ def _make_agent(self, session_db):
                     skip_context_files=True,
                     skip_memory=True,
                 )
    +        # Simulate lazy session creation (normally done by run_conversation)
    +        agent._ensure_db_session()
             return agent
     
         def test_flush_writes_only_new_messages(self):
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -280,7 +280,7 @@ def _notify_session_boundary(event_type: str, session_id: str | None) -> None:
             pass
     
     
    -def _finalize_session(session: dict | None) -> None:
    +def _finalize_session(session: dict | None, end_reason: str = "tui_close") -> None:
         """Best-effort finalize hook + memory commit for a session."""
         if not session or session.get("_finalized"):
             return
    ```

    ```diff
    @@ -299,13 +299,24 @@ def _finalize_session(session: dict | None) -> None:
             except Exception:
                 pass
     
    -    session_id = getattr(agent, "session_id", None) or session.get("session_key")
    +    session_key = session.get("session_key")
    +    session_id = getattr(agent, "session_id", None) or session_key
         _notify_session_boundary("on_session_finalize", session_id)
     
    +    # Mark session ended in DB so it doesn't linger as a ghost row in /resume.
    +    # Adapted from #18283 (luyao618) and #18299 (Bartok9).
    +    if session_key:
    +        try:
    +            db = _get_db()
    +            if db is not None:
    +                db.end_session(session_key, end_reason)
    +        except Exception:
    +            pass
    +
     
     def _shutdown_sessions() -> None:
         for session in list(_sessions.values()):
    -        _finalize_session(session)
    +        _finalize_session(session, end_reason="tui_shutdown")
             try:
                 worker = session.get("slash_worker")
                 if worker:
    ```

    ```diff
    @@ -539,32 +550,8 @@ def _build() -> None:
                 finally:
                     _clear_session_context(tokens)
     
    -            db = _get_db()
    -            if db is not None:
    -                db.create_session(key, source="tui", model=_resolve_model())
    -                pending_title = (current.get("pending_title") or "").strip()
    -                if pending_title:
    -                    try:
    -                        title_applied = db.set_session_title(key, pending_title)
    -                        if title_applied:
    -                            current["pending_title"] = None
    -                        else:
    -                            existing_row = db.get_session(key)
    -                            existing_title = ((existing_row or {}).get("title") or "").strip()
    -                            if existing_title == pending_title:
    -                                current["pending_title"] = None
    -                            else:
    -                                logger.info(
    -                                    "Pending title still queued for session %s (wanted=%r, current=%r)",
    -                                    sid,
    -                                    pending_title,
    -                                    existing_title,
    -                                )
    -                    except ValueError as e:
    -                        current["pending_title"] = None
    -                        logger.info("Dropping pending title for session %s: %s", sid, e)
    -                    except Exception:
    -                        logger.warning("Failed to apply pending title for session %s", sid, exc_info=True)
    +            # Session DB row deferred to first run_conversation() call.
    +            # pending_title applied post-first-message (see cli.exec handler).
                 current["agent"] = agent
     
                 try:
    ```

    ```diff
    @@ -2996,6 +2983,17 @@ def _stream(delta):
                     payload["rendered"] = rendered
                 _emit("message.complete", sid, payload)
     
    +            # Apply pending_title now that the DB row exists.
    +            _pending = session.get("pending_title")
    +            if _pending and status == "complete":
    +                _pdb = _get_db()
    +                if _pdb:
    +                    try:
    +                        if _pdb.set_session_title(session.get("session_key") or sid, _pending):
    +                            session["pending_title"] = None
    +                    except Exception:
    +                        pass  # Best effort — auto-title will handle it below
    +
                 if (
                     status == "complete"
                     and isinstance(raw, str)
    ```
  - `uv.lock`

    ```diff
    @@ -9,7 +9,7 @@ resolution-markers = [
     ]
     
     [options]
    -exclude-newer = "2026-04-17T16:49:45.944715922Z"
    +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
     exclude-newer-span = "P7D"
     
     [[package]]
    ```

    ```diff
    @@ -156,6 +156,19 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload-time = "2024-11-06T10:44:52.917Z" },
     ]
     
    +[[package]]
    +name = "aiohttp-socks"
    +version = "0.11.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "aiohttp" },
    +    { name = "python-socks" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/1f/cc/e5bbd54f76bd56291522251e47267b645dac76327b2657ade9545e30522c/aiohttp_socks-0.11.0.tar.gz", hash = "sha256:0afe51638527c79077e4bd6e57052c87c4824233d6e20bb061c53766421b10f0", size = 11196, upload-time = "2025-12-09T13:35:52.564Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/bf/7d/4b633d709b8901d59444d2e512b93e72fe62d2b492a040097c3f7ba017bb/aiohttp_socks-0.11.0-py3-none-any.whl", hash = "sha256:9aacce57c931b8fbf8f6d333cf3cafe4c35b971b35430309e167a35a8aab9ec1", size = 10556, upload-time = "2025-12-09T13:35:50.18Z" },
    +]
    +
     [[package]]
     name = "aiosignal"
     version = "1.4.0"
    ```

    ```diff
    @@ -1759,6 +1772,77 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
     ]
     
    +[[package]]
    +name = "google-api-core"
    +version = "2.30.3"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "google-auth" },
    +    { name = "googleapis-common-protos" },
    +    { name = "proto-plus" },
    +    { name = "protobuf" },
    +    { name = "requests" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" },
    +]
    +
    +[[package]]
    +name = "google-api-python-client"
    +version = "2.194.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "google-api-core" },
    +    { name = "google-auth" },
    +    { name = "google-auth-httplib2" },
    +    { name = "httplib2" },
    +    { name = "uritemplate" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/60/ab/e83af0eb043e4ccc49571ca7a6a49984e9d00f4e9e6e6f1238d60bc84dce/google_api_python_client-2.194.0.tar.gz", hash = "sha256:db92647bd1a90f40b79c9618461553c2b20b6a43ce7395fa6de07132dc14f023", size = 14443469, upload-time = "2026-04-08T23:07:35.757Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/b0/34/5a624e49f179aa5b0cb87b2ce8093960299030ff40423bfbde09360eb908/google_api_python_client-2.194.0-py3-none-any.whl", hash = "sha256:61eaaac3b8fc8fdf11c08af87abc3d1342d1b37319cc1b57405f86ef7697e717", size = 15016514, upload-time = "2026-04-08T23:07:33.093Z" },
    +]
    +
    +[[package]]
    +name = "google-auth"
    +version = "2.49.2"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "cryptography" },
    +    { name = "pyasn1-modules" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" },
    +]
    +
    +[[package]]
    +name = "google-auth-httplib2"
    +version = "0.3.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "google-auth" },
    +    { name = "httplib2" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/ed/99/107612bef8d24b298bb5a7c8466f908ecda791d43f9466f5c3978f5b24c1/google_auth_httplib2-0.3.1.tar.gz", hash = "sha256:0af542e815784cb64159b4469aa5d71dd41069ba93effa006e1916b1dcd88e55", size = 11152, upload-time = "2026-03-30T22:50:26.766Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/97/e9/93afb14d23a949acaa3f4e7cc51a0024671174e116e35f42850764b99634/google_auth_httplib2-0.3.1-py3-none-any.whl", hash = "sha256:682356a90ef4ba3d06548c37e9112eea6fc00395a11b0303a644c1a86abc275c", size = 9534, upload-time = "2026-03-30T22:49:03.384Z" },
    +]
    +
    +[[package]]
    +name = "google-auth-oauthlib"
    +version = "1.3.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "google-auth" },
    +    { name = "requests-oauthlib" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" },
    +]
    +
     [[package]]
     name = "googleapis-common-protos"
     version = "1.73.0"
    ```

    ```diff
    @@ -1870,10 +1954,11 @@ wheels = [
     
     [[package]]
     name = "hermes-agent"
    -version = "0.11.0"
    +version = "0.12.0"
     source = { editable = "." }
     dependencies = [
         { name = "anthropic" },
    +    { name = "croniter" },
         { name = "edge-tts" },
         { name = "exa-py" },
         { name = "fal-client" },
    ```

    ```diff
    @@ -1900,11 +1985,11 @@ acp = [
     all = [
         { name = "agent-client-protocol" },
         { name = "aiohttp" },
    +    { name = "aiohttp-socks", marker = "sys_platform == 'linux'" },
         { name = "aiosqlite", marker = "sys_platform == 'linux'" },
         { name = "alibabacloud-dingtalk" },
         { name = "asyncpg", marker = "sys_platform == 'linux'" },
         { name = "boto3" },
    -    { name = "croniter" },
         { name = "daytona" },
         { name = "debugpy" },
         { name = "dingtalk-stream" },
    ```

    ```diff
    @@ -1912,6 +1997,9 @@ all = [
         { name = "elevenlabs" },
         { name = "fastapi" },
         { name = "faster-whisper" },
    +    { name = "google-api-python-client" },
    +    { name = "google-auth-httplib2" },
    +    { name = "google-auth-oauthlib" },
         { name = "honcho-ai" },
         { name = "lark-oapi" },
         { name = "markdown", marker = "sys_platform == 'linux'" },
    ```

    ```diff
    @@ -1942,9 +2030,6 @@ bedrock = [
     cli = [
         { name = "simple-term-menu" },
     ]
    -cron = [
    -    { name = "croniter" },
    -]
     daytona = [
         { name = "daytona" },
     ]
    ```

    ```diff
    @@ -1966,6 +2051,11 @@ feishu = [
         { name = "lark-oapi" },
         { name = "qrcode" },
     ]
    +google = [
    +    { name = "google-api-python-client" },
    +    { name = "google-auth-httplib2" },
    +    { name = "google-auth-oauthlib" },
    +]
     homeassistant = [
         { name = "aiohttp" },
     ]
    ```

    ```diff
    @@ -1973,6 +2063,7 @@ honcho = [
         { name = "honcho-ai" },
     ]
     matrix = [
    +    { name = "aiohttp-socks" },
         { name = "aiosqlite" },
         { name = "asyncpg" },
         { name = "markdown" },
    ```

    ```diff
    @@ -2015,7 +2106,6 @@ sms = [
     ]
     termux = [
         { name = "agent-client-protocol" },
    -    { name = "croniter" },
         { name = "honcho-ai" },
         { name = "mcp" },
         { name = "ptyprocess", marker = "sys_platform != 'win32'" },
    ```

    ```diff
    @@ -2048,13 +2138,14 @@ requires-dist = [
         { name = "aiohttp", marker = "extra == 'homeassistant'", specifier = ">=3.9.0,<4" },
         { name = "aiohttp", marker = "extra == 'messaging'", specifier = ">=3.13.3,<4" },
         { name = "aiohttp", marker = "extra == 'sms'", specifier = ">=3.9.0,<4" },
    +    { name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = ">=0.10,<1" },
         { name = "aiosqlite", marker = "extra == 'matrix'", specifier = ">=0.20" },
         { name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = ">=2.0.0" },
         { name = "anthropic", specifier = ">=0.39.0,<1" },
         { name = "asyncpg", marker = "extra == 'matrix'", specifier = ">=0.29" },
         { name = "atroposlib", marker = "extra == 'rl'", git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30" },
         { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.35.0,<2" },
    -    { name = "croniter", marker = "extra == 'cron'", specifier = ">=6.0.0,<7" },
    +    { name = "croniter", specifier = ">=6.0.0,<7" },
         { name = "daytona", marker = "extra == 'daytona'", specifier = ">=0.148.0,<1" },
         { name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.0,<2" },
         { name = "dingtalk-stream", marker = "extra == 'dingtalk'", specifier = ">=0.20,<1" },
    ```

    ```diff
    @@ -2068,6 +2159,9 @@ requires-dist = [
         { name = "faster-whisper", marker = "extra == 'voice'", specifier = ">=1.0.0,<2" },
         { name = "fire", specifier = ">=0.7.1,<1" },
         { name = "firecrawl-py", specifier = ">=4.16.0,<5" },
    +    { name = "google-api-python-client", marker = "extra == 'google'", specifier = ">=2.100,<3" },
    +    { name = "google-auth-httplib2", marker = "extra == 'google'", specifier = ">=0.2,<1" },
    +    { name = "google-auth-oauthlib", marker = "extra == 'google'", specifier = ">=1.0,<2" },
         { name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
         { name = "hermes-agent", extras = ["bedrock"], marker = "extra == 'all'" },
    ```

    ```diff
    @@ -2079,6 +2173,7 @@ requires-dist = [
         { name = "hermes-agent", extras = ["dev"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["dingtalk"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["feishu"], marker = "extra == 'all'" },
    +    { name = "hermes-agent", extras = ["google"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["homeassistant"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'all'" },
         { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'termux'" },
    ```

    ```diff
    @@ -2142,7 +2237,7 @@ requires-dist = [
         { name = "wandb", marker = "extra == 'rl'", specifier = ">=0.15.0,<1" },
         { name = "yc-bench", marker = "python_full_version >= '3.12' and extra == 'yc-bench'", git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c" },
     ]
    -provides-extras = ["modal", "daytona", "vercel", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "bedrock", "termux", "dingtalk", "feishu", "web", "rl", "yc-bench", "all"]
    +provides-extras = ["modal", "daytona", "vercel", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "bedrock", "termux", "dingtalk", "feishu", "google", "web", "rl", "yc-bench", "all"]
     
     [[package]]
     name = "hf-transfer"
    ```

    ```diff
    @@ -2244,6 +2339,18 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
     ]
     
    +[[package]]
    +name = "httplib2"
    +version = "0.31.2"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "pyparsing" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" },
    +]
    +
     [[package]]
     name = "httptools"
     version = "0.7.1"
    ```

    ```diff
    @@ -3283,6 +3390,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
     ]
     
    +[[package]]
    +name = "oauthlib"
    +version = "3.3.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
    +]
    +
     [[package]]
     name = "obstore"
     version = "0.8.2"
    ```

    ```diff
    @@ -3861,6 +3977,18 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
     ]
     
    +[[package]]
    +name = "proto-plus"
    +version = "1.27.2"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "protobuf" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" },
    +]
    +
     [[package]]
     name = "protobuf"
     version = "6.33.5"
    ```

    ```diff
    @@ -3935,6 +4063,27 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" },
     ]
     
    +[[package]]
    +name = "pyasn1"
    +version = "0.6.3"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
    +]
    +
    +[[package]]
    +name = "pyasn1-modules"
    +version = "0.4.2"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "pyasn1" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
    +]
    +
     [[package]]
     name = "pycparser"
     version = "3.0"
    ```

    ```diff
    @@ -4275,6 +4424,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/79/93/f6729f10149305262194774d6c8b438c0b084740cf239f48ab97b4df02fa/python_olm-3.2.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a5e68a2f4b5a2bfa5fdb5dbfa22396a551730df6c4a572235acaa96e997d3f", size = 297000, upload-time = "2023-11-28T19:25:31.045Z" },
     ]
     
    +[[package]]
    +name = "python-socks"
    +version = "2.8.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" },
    +]
    +
     [[package]]
     name = "python-telegram-bot"
     version = "22.6"
    ```

    ```diff
    @@ -4535,6 +4693,19 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
     ]
     
    +[[package]]
    +name = "requests-oauthlib"
    +version = "2.0.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "oauthlib" },
    +    { name = "requests" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
    +]
    +
     [[package]]
     name = "requests-toolbelt"
     version = "1.0.0"
    ```

    ```diff
    @@ -5274,6 +5445,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/4c/a7/563b2d8fb7edc07320bf69ac6a7eedcd7a1a9d663a6bb90a4d9bd2eda5f7/unpaddedbase64-2.1.0-py3-none-any.whl", hash = "sha256:485eff129c30175d2cd6f0cd8d2310dff51e666f7f36175f738d75dfdbd0b1c6", size = 6083, upload-time = "2021-03-09T11:35:46.7Z" },
     ]
     
    +[[package]]
    +name = "uritemplate"
    +version = "4.2.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
    +]
    +
     [[package]]
     name = "urllib3"
     version = "2.6.3"
    ```
- **#18283: fix: end TUI sessions in state.db to prevent ghost rows in /resume**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#18370`
- **#18299: fix(tui): end_session on finalize to prevent ghost sessions in /resume (#18269)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#18370`

## Fix status

- Merged to main in PR #18370 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-01`.
