mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 20:29:00 +08:00
Compare commits
1 Commits
feat/plugi
...
bb/cron-hi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cd8a83ae8 |
@@ -42,6 +42,7 @@ import type {
|
||||
} from '@/types/hermes'
|
||||
|
||||
const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000
|
||||
const CRON_API_TIMEOUT_MS = 60_000
|
||||
|
||||
export type {
|
||||
ActionResponse,
|
||||
@@ -495,7 +496,8 @@ export function testMessagingPlatform(platformId: string): Promise<MessagingPlat
|
||||
|
||||
export function getCronJobs(): Promise<CronJob[]> {
|
||||
return window.hermesDesktop.api<CronJob[]>({
|
||||
path: '/api/cron/jobs'
|
||||
path: '/api/cron/jobs',
|
||||
timeoutMs: CRON_API_TIMEOUT_MS
|
||||
})
|
||||
}
|
||||
|
||||
@@ -507,7 +509,8 @@ export function getCronJob(jobId: string): Promise<CronJob> {
|
||||
|
||||
export async function getCronJobRuns(jobId: string, limit = 20): Promise<SessionInfo[]> {
|
||||
const { runs } = await window.hermesDesktop.api<{ runs: SessionInfo[] }>({
|
||||
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/runs?limit=${limit}`
|
||||
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/runs?limit=${limit}`,
|
||||
timeoutMs: CRON_API_TIMEOUT_MS
|
||||
})
|
||||
|
||||
return runs ?? []
|
||||
|
||||
@@ -5676,12 +5676,11 @@ async def list_cron_job_runs(job_id: str, profile: Optional[str] = None, limit:
|
||||
|
||||
db = _open_session_db_for_profile(selected)
|
||||
try:
|
||||
runs = db.list_sessions_rich(
|
||||
runs = db.list_sessions_rich_by_id_prefix(
|
||||
f"cron_{canonical}_",
|
||||
source="cron",
|
||||
id_query=f"cron_{canonical}_",
|
||||
limit=limit_n,
|
||||
offset=0,
|
||||
order_by_last_active=True,
|
||||
)
|
||||
now = time.time()
|
||||
for s in runs:
|
||||
|
||||
@@ -1880,6 +1880,82 @@ class SessionDB:
|
||||
s["preview"] = ""
|
||||
return s
|
||||
|
||||
def list_sessions_rich_by_id_prefix(
|
||||
self,
|
||||
id_prefix: str,
|
||||
*,
|
||||
source: str = None,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
include_archived: bool = False,
|
||||
archived_only: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""List rich session rows whose IDs start with ``id_prefix``.
|
||||
|
||||
Fast path for namespaces encoded directly in session IDs (for example
|
||||
cron run sessions ``cron_<job_id>_<timestamp>``). Uses ``LIKE 'prefix%'``
|
||||
so SQLite can leverage the primary-key index on ``sessions.id`` instead
|
||||
of the heavier substring/chain path in ``list_sessions_rich(id_query=...)``.
|
||||
"""
|
||||
prefix = (id_prefix or "").strip()
|
||||
if not prefix or limit <= 0:
|
||||
return []
|
||||
|
||||
escaped_prefix = (
|
||||
prefix
|
||||
.replace("\\", "\\\\")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_")
|
||||
)
|
||||
|
||||
where_clauses = ["s.id LIKE ? ESCAPE '\\'"]
|
||||
params: List[Any] = [f"{escaped_prefix}%"]
|
||||
|
||||
if source:
|
||||
where_clauses.append("s.source = ?")
|
||||
params.append(source)
|
||||
if archived_only:
|
||||
where_clauses.append("s.archived = 1")
|
||||
elif not include_archived:
|
||||
where_clauses.append("s.archived = 0")
|
||||
|
||||
where_sql = f"WHERE {' AND '.join(where_clauses)}"
|
||||
query = f"""
|
||||
SELECT s.*,
|
||||
COALESCE(
|
||||
(SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
|
||||
FROM messages m
|
||||
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
|
||||
ORDER BY m.timestamp, m.id LIMIT 1),
|
||||
''
|
||||
) AS _preview_raw,
|
||||
COALESCE(
|
||||
(SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
|
||||
s.started_at
|
||||
) AS last_active
|
||||
FROM sessions s
|
||||
{where_sql}
|
||||
ORDER BY last_active DESC, s.started_at DESC, s.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
params.extend([limit, offset])
|
||||
|
||||
with self._lock:
|
||||
cursor = self._conn.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
sessions: List[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
s = dict(row)
|
||||
raw = s.pop("_preview_raw", "").strip()
|
||||
if raw:
|
||||
text = raw[:60]
|
||||
s["preview"] = text + ("..." if len(raw) > 60 else "")
|
||||
else:
|
||||
s["preview"] = ""
|
||||
sessions.append(s)
|
||||
return sessions
|
||||
|
||||
# =========================================================================
|
||||
# Message storage
|
||||
# =========================================================================
|
||||
|
||||
@@ -197,3 +197,41 @@ async def test_cron_profile_validation_errors(isolated_profiles):
|
||||
with pytest.raises(HTTPException) as missing:
|
||||
await web_server.list_cron_jobs(profile="missing_profile")
|
||||
assert missing.value.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_cron_job_runs_filters_prefix_and_respects_limit(isolated_profiles):
|
||||
from hermes_cli import web_server
|
||||
from hermes_state import SessionDB
|
||||
|
||||
worker_job = web_server._call_cron_for_profile(
|
||||
"worker_alpha",
|
||||
"create_job",
|
||||
prompt="worker run history",
|
||||
schedule="every 30m",
|
||||
name="worker-runs",
|
||||
)
|
||||
prefix = f"cron_{worker_job['id']}_"
|
||||
|
||||
worker_db = SessionDB(db_path=isolated_profiles["worker_alpha"] / "state.db")
|
||||
default_db = SessionDB(db_path=isolated_profiles["default"] / "state.db")
|
||||
try:
|
||||
worker_db.create_session(f"{prefix}20260606_100001", source="cron")
|
||||
worker_db.create_session("cron_other_job_20260606_100002", source="cron")
|
||||
worker_db.create_session(f"{prefix}20260606_100003", source="cron")
|
||||
|
||||
# Same prefix in another profile must not leak into worker results.
|
||||
default_db.create_session(f"{prefix}20260606_100004", source="cron")
|
||||
finally:
|
||||
worker_db.close()
|
||||
default_db.close()
|
||||
|
||||
payload = await web_server.list_cron_job_runs(worker_job["id"], limit=2)
|
||||
runs = payload["runs"]
|
||||
|
||||
assert payload["limit"] == 2
|
||||
assert [run["id"] for run in runs] == [
|
||||
f"{prefix}20260606_100003",
|
||||
f"{prefix}20260606_100001",
|
||||
]
|
||||
assert all(run.get("profile") == "worker_alpha" for run in runs)
|
||||
|
||||
Reference in New Issue
Block a user