From c5b4c481656634ff919b214a037b830077d3bbd1 Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Fri, 1 May 2026 18:39:12 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20lazy=20session=20creation=20=E2=80=94=20?= =?UTF-8?q?defer=20DB=20row=20until=20first=20message=20(#18370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents ghost sessions from accumulating in state.db when the TUI/web dashboard is opened and closed without sending a message. Changes: - run_agent.py: Add _ensure_db_session() gate method, called at run_conversation() entry. Remove eager create_session() from __init__. Handle compression rotation flag correctly. - tui_gateway/server.py: Remove eager db.create_session() in _start_agent_build(). Add post-first-message pending_title re-apply. - hermes_state.py: Extract _insert_session_row() shared helper (DRY). Add prune_empty_ghost_sessions() for one-time migration. - cli.py: One-time ghost session prune on startup. Fix _pending_title to call _ensure_db_session() before set_session_title(). - hermes_cli/main.py: Guard TUI exit summary on message_count > 0. - tests: Update test_860_dedup to call _ensure_db_session() before direct _flush_messages_to_session_db() calls. Closes: ghost session clutter in hermes sessions list and web dashboard. --- cli.py | 32 ++++- hermes_cli/main.py | 2 + hermes_state.py | 57 ++++++--- run_agent.py | 68 +++++----- tests/run_agent/test_860_dedup.py | 2 + tui_gateway/server.py | 56 ++++----- uv.lock | 198 ++++++++++++++++++++++++++++-- 7 files changed, 322 insertions(+), 93 deletions(-) diff --git a/cli.py b/cli.py index 9ff6b8708a..f0ba6fc991 100644 --- a/cli.py +++ b/cli.py @@ -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 @@ -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) @@ -3618,14 +3632,18 @@ class HermesCLI: 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}[/]") @@ -4953,6 +4971,7 @@ class HermesCLI: 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"), @@ -4962,6 +4981,7 @@ class HermesCLI: "reasoning_config": self.reasoning_config, }, ) + self.agent._session_db_created = True except Exception: pass # Notify memory providers that session_id rotated to a fresh diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 5598a1f3ff..72a958b573 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -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 = int(session.get("input_tokens") or 0) output_tokens = int(session.get("output_tokens") or 0) cache_read_tokens = int(session.get("cache_read_tokens") or 0) diff --git a/hermes_state.py b/hermes_state.py index b3e00b9ff6..2cfd13d6d5 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -514,7 +514,7 @@ class SessionDB: # Session lifecycle # ========================================================================= - def create_session( + def _insert_session_row( self, session_id: str, source: str, @@ -523,8 +523,8 @@ class SessionDB: 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, @@ -542,8 +542,11 @@ class SessionDB: ), ) 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. @@ -679,21 +682,41 @@ class SessionDB: 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.""" diff --git a/run_agent.py b/run_agent.py index 26933994d4..1d926050fc 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1632,30 +1632,12 @@ class AIAgent: 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": max_tokens, - }, - 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": max_tokens, + } # In-memory todo list for task planning (one per agent/session) from tools.todo_tool import TodoStore @@ -2170,6 +2152,28 @@ class AIAgent: "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. @@ -3719,14 +3723,9 @@ class AIAgent: 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:]: @@ -9056,12 +9055,15 @@ class AIAgent: 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: @@ -10351,6 +10353,8 @@ class AIAgent: # 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 `` can filter a single conversation. from hermes_logging import set_session_context diff --git a/tests/run_agent/test_860_dedup.py b/tests/run_agent/test_860_dedup.py index 89f4c010b6..cf9b8e745c 100644 --- a/tests/run_agent/test_860_dedup.py +++ b/tests/run_agent/test_860_dedup.py @@ -38,6 +38,8 @@ class TestFlushDeduplication: 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): diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 523655d4b9..724fb542e6 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -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 @@ -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: @@ -539,32 +550,8 @@ def _start_agent_build(sid: str, session: dict) -> 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: @@ -2994,6 +2981,17 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None: 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) diff --git a/uv.lock b/uv.lock index 93db335ce9..6910c1ec75 100644 --- a/uv.lock +++ b/uv.lock @@ -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]] @@ -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" @@ -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" @@ -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" }, @@ -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" }, @@ -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'" }, @@ -1942,9 +2030,6 @@ bedrock = [ cli = [ { name = "simple-term-menu" }, ] -cron = [ - { name = "croniter" }, -] daytona = [ { name = "daytona" }, ] @@ -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" }, ] @@ -1973,6 +2063,7 @@ honcho = [ { name = "honcho-ai" }, ] matrix = [ + { name = "aiohttp-socks" }, { name = "aiosqlite" }, { name = "asyncpg" }, { name = "markdown" }, @@ -2015,7 +2106,6 @@ sms = [ ] termux = [ { name = "agent-client-protocol" }, - { name = "croniter" }, { name = "honcho-ai" }, { name = "mcp" }, { name = "ptyprocess", marker = "sys_platform != 'win32'" }, @@ -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" }, @@ -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'" }, @@ -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'" }, @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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"