Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3774,9 +3774,18 @@ def new_session(self, silent=False):
except (Exception, KeyboardInterrupt):
pass
self._notify_session_boundary("on_session_finalize")
# Commit OpenViking session so memories become searchable
try:
self.agent.shutdown_memory_provider(self.conversation_history)
except Exception:
pass
elif self.agent:
# First session or empty history — still finalize the old session
self._notify_session_boundary("on_session_finalize")
try:
self.agent.shutdown_memory_provider()
except Exception:
pass

old_session_id = self.session_id
if self._session_db and old_session_id:
Expand Down
81 changes: 81 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from datetime import datetime
from typing import Dict, Optional, Any, List

import httpx

# ---------------------------------------------------------------------------
# SSL certificate auto-detection for NixOS and other non-standard systems.
# Must run BEFORE any HTTP library (discord, aiohttp, etc.) is imported.
Expand Down Expand Up @@ -1247,6 +1249,9 @@ async def start(self) -> bool:

# Start background session expiry watcher for proactive memory flushing
asyncio.create_task(self._session_expiry_watcher())

# Start background idle commit watcher for early memory searchability
asyncio.create_task(self._idle_commit_watcher())

# Start background reconnection watcher for platforms that failed at startup
if self._failed_platforms:
Expand Down Expand Up @@ -1364,6 +1369,82 @@ async def _session_expiry_watcher(self, interval: int = 300):
break
await asyncio.sleep(1)

async def _idle_commit_watcher(self, interval: int = 60, idle_seconds: int = 120):
"""Background task that commits idle sessions for early memory searchability.

Runs every `interval` seconds (default 60s). For each session that has been
idle for `idle_seconds` (default 120s = 2 min), commits the OpenViking session
so memories become searchable before the full session expiry timeout.

This allows users to search recent conversations without waiting for the
full 2-hour session timeout.
"""
await asyncio.sleep(30) # initial delay — let the gateway fully start

# Get OpenViking endpoint from config (supports remote deployments)
_endpoint = os.environ.get("OPENVIKING_ENDPOINT", "http://127.0.0.1:1933")
_api_key = os.environ.get('OPENVIKING_API_KEY', '')

while self._running:
try:
self.session_store._ensure_loaded()
now = datetime.now()
_committed_count = 0

# Collect sessions to commit (avoid modifying dict during iteration)
_to_commit = []
for key, entry in list(self.session_store._entries.items()):
# Skip if already committed or flushed
if entry.memory_committed or entry.memory_flushed:
continue

# Check if session has been idle long enough
idle_time = (now - entry.updated_at).total_seconds()
if idle_time >= idle_seconds:
_to_commit.append((entry, idle_time))

if _to_commit:
# Use async httpx client for non-blocking HTTP requests
_headers = {}
if _api_key:
_headers['Authorization'] = f'Bearer {_api_key}'

async with httpx.AsyncClient(timeout=10.0) as client:
for entry, idle_time in _to_commit:
try:
_resp = await client.post(
f"{_endpoint}/api/v1/sessions/{entry.session_id}/commit",
headers=_headers,
)
if _resp.status_code == 200:
_committed_count += 1
with self.session_store._lock:
entry.memory_committed = True
self.session_store._save()
logger.info(
"Idle commit: session %s committed after %.0fs idle",
entry.session_id, idle_time,
)
else:
logger.debug(
"Idle commit failed for session %s: HTTP %d",
entry.session_id, _resp.status_code,
)
except Exception as e:
logger.debug("Idle commit failed for session %s: %s", entry.session_id, e)

if _committed_count:
logger.info("Idle commit watcher: %d session(s) committed", _committed_count)

except Exception as e:
logger.debug("Idle commit watcher error: %s", e)

# Sleep in small increments so we can stop quickly
for _ in range(interval):
if not self._running:
break
await asyncio.sleep(1)

async def _platform_reconnect_watcher(self) -> None:
"""Background task that periodically retries connecting failed platforms.

Expand Down
10 changes: 10 additions & 0 deletions gateway/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ class SessionEntry:
# set was lost on restart, causing redundant re-flushes).
memory_flushed: bool = False

# Set by the idle commit watcher when the session has been idle long
# enough to trigger an early memory commit (for searchability).
# Reset to False when a new message arrives.
memory_committed: bool = False

def to_dict(self) -> Dict[str, Any]:
result = {
"session_key": self.session_key,
Expand All @@ -387,6 +392,7 @@ def to_dict(self) -> Dict[str, Any]:
"estimated_cost_usd": self.estimated_cost_usd,
"cost_status": self.cost_status,
"memory_flushed": self.memory_flushed,
"memory_committed": self.memory_committed,
}
if self.origin:
result["origin"] = self.origin.to_dict()
Expand Down Expand Up @@ -423,6 +429,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "SessionEntry":
estimated_cost_usd=data.get("estimated_cost_usd", 0.0),
cost_status=data.get("cost_status", "unknown"),
memory_flushed=data.get("memory_flushed", False),
memory_committed=data.get("memory_committed", False),
)


Expand Down Expand Up @@ -769,6 +776,9 @@ def update_session(
entry.updated_at = _now()
if last_prompt_tokens is not None:
entry.last_prompt_tokens = last_prompt_tokens
# Reset memory_committed flag - user has new activity
if entry.memory_committed:
entry.memory_committed = False
self._save()

def reset_session(self, session_key: str) -> Optional[SessionEntry]:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions hermes_cli/web_dist/assets/index-5Ztth3H1.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions hermes_cli/web_dist/assets/index-D90JydVU.css

Large diffs are not rendered by default.

Binary file added hermes_cli/web_dist/ds-assets/filler-bg0.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hermes_cli/web_dist/favicon.ico
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Collapse-Bold.woff2
Binary file not shown.
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Collapse-Italic.woff2
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Collapse-Light.woff2
Binary file not shown.
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Collapse-Regular.woff2
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Collapse-Thin.woff2
Binary file not shown.
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Mondwest-Regular.woff2
Binary file not shown.
Binary file added hermes_cli/web_dist/fonts/Neuebit-Bold.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
14 changes: 14 additions & 0 deletions hermes_cli/web_dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hermes Agent - Dashboard</title>
<script type="module" crossorigin src="/assets/index-5Ztth3H1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D90JydVU.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
9 changes: 9 additions & 0 deletions ui-tui/dist/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { GatewayProvider } from './app/gatewayContext.js';
import { useMainApp } from './app/useMainApp.js';
import { AppLayout } from './components/appLayout.js';
import { MOUSE_TRACKING } from './config/env.js';
export function App({ gw }) {
const { appActions, appComposer, appProgress, appStatus, appTranscript, gateway } = useMainApp(gw);
return (_jsx(GatewayProvider, { value: gateway, children: _jsx(AppLayout, { actions: appActions, composer: appComposer, mouseTracking: MOUSE_TRACKING, progress: appProgress, status: appStatus, transcript: appTranscript }) }));
}
Loading