Compare commits

...

4 Commits

Author SHA1 Message Date
7ac8d0268f test(cron): add tests for cloud-context warning injection
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 1m13s
2026-04-14 01:18:16 +00:00
2e59f8540d fix(cron): inject cloud-context warning when prompt refs localhost
Fixes #378, Closes #456

When a cron job runs on a cloud endpoint but its prompt references
local services (Ollama, localhost ports, etc.), inject a SYSTEM NOTE
telling the agent it cannot reach localhost so it reports the
limitation instead of wasting iterations on doomed connections.
2026-04-14 01:17:25 +00:00
954fd992eb Merge pull request 'perf: lazy session creation — defer DB write until first message (#314)' (#449) from whip/314-1776127532 into main
Some checks failed
Forge CI / smoke-and-build (push) Failing after 55s
Forge CI / smoke-and-build (pull_request) Failing after 1m12s
perf: lazy session creation (#314)

Closes #314.
2026-04-14 01:08:13 +00:00
Metatron
f35f56e397 perf: lazy session creation — defer DB write until first message (closes #314)
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 56s
Remove eager create_session() call from AIAgent.__init__(). Sessions
are now created lazily on first _flush_messages_to_session_db() call
via ensure_session() which uses INSERT OR IGNORE.

Impact: eliminates 32.4% of sessions (3,564 of 10,985) that were
created at agent init but never received any messages.

The existing ensure_session() fallback in _flush_messages_to_session_db()
already handles this pattern — it was originally designed for recovery
after transient SQLite lock failures. Now it's the primary creation path.

Compression-initiated sessions still use create_session() directly
(line ~5995) since they have messages to write immediately.
2026-04-13 20:52:06 -04:00
3 changed files with 157 additions and 24 deletions

View File

@@ -12,6 +12,7 @@ import asyncio
import concurrent.futures
import json
import logging
import re
import os
import subprocess
import sys
@@ -544,6 +545,55 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
except Exception as exc:
return False, f"Script execution failed: {exc}"
# ---------------------------------------------------------------------------
# Cloud-context warning for local-service references (#378, #456)
# ---------------------------------------------------------------------------
_LOCAL_SERVICE_PATTERNS = [
re.compile(r'localhost:\d+', re.IGNORECASE),
re.compile(r'127\.0\.0\.1:\d+'),
re.compile(r'check\s+ollama', re.IGNORECASE),
re.compile(r'ollama\s+(is\s+)?respond', re.IGNORECASE),
re.compile(r'curl\s+localhost', re.IGNORECASE),
re.compile(r'curl\s+127\.', re.IGNORECASE),
re.compile(r'curl\s+local', re.IGNORECASE),
re.compile(r'ping\s+localhost', re.IGNORECASE),
re.compile(r'poll(ing)?\s+local', re.IGNORECASE),
re.compile(r'check\s+service\s+respond', re.IGNORECASE),
re.compile(r'11434'), # Ollama default port
re.compile(r'11435'), # common alt Ollama port
]
def _detect_local_service_refs(prompt: str) -> list[str]:
"""Return list of local-service reference descriptions found in prompt."""
refs = []
for pat in _LOCAL_SERVICE_PATTERNS:
m = pat.search(prompt)
if m:
refs.append(m.group(0))
return refs
def _inject_cloud_context(prompt: str, refs: list[str], provider: str) -> str:
"""Prepend a SYSTEM NOTE so the agent knows it cannot reach localhost."""
refs_str = ", ".join(f'"{r}"' for r in refs)
warning = (
"[SYSTEM NOTE — cloud endpoint]
"
f"You are running on a cloud inference endpoint ({provider}). "
f"Your prompt references local services: {refs_str}. "
"You CANNOT reach localhost or any local network address from this endpoint. "
"Do NOT attempt curl, ping, SSH, or any network calls to localhost. "
"Instead, report to the user that this job requires a local model endpoint "
"to check local services, and suggest they re-run with a local provider.
"
)
return warning + prompt
def _build_job_prompt(job: dict) -> str:
"""Build the effective prompt for a cron job, optionally loading one or more skills first."""
@@ -817,6 +867,18 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
job_name,
)
# Inject cloud-context warning when prompt references local services (#378)
if _is_cloud:
_local_refs = _detect_local_service_refs(prompt)
if _local_refs:
_provider_name = turn_route["runtime"].get("provider", "cloud")
prompt = _inject_cloud_context(prompt, _local_refs, _provider_name)
logger.info(
"Job '%s': injected cloud-context warning for local refs: %s",
job_name,
_local_refs,
)
_agent_kwargs = _safe_agent_kwargs({
"model": turn_route["model"],
"api_key": turn_route["runtime"].get("api_key"),

View File

@@ -1001,30 +1001,10 @@ 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
)
# Lazy session creation: defer until first message flush (#314).
# _flush_messages_to_session_db() calls ensure_session() which uses
# INSERT OR IGNORE — creating the row only when messages arrive.
# This eliminates 32% of sessions that are created but never used.
# In-memory todo list for task planning (one per agent/session)
from tools.todo_tool import TodoStore

View File

@@ -0,0 +1,91 @@
"""Tests for cloud-context warning injection (#378, #456)."""
import pytest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from cron.scheduler import (
_LOCAL_SERVICE_PATTERNS,
_detect_local_service_refs,
_inject_cloud_context,
)
class TestDetectLocalServiceRefs:
"""Pattern detection for local service references in prompts."""
def test_localhost_with_port(self):
refs = _detect_local_service_refs("Check localhost:11434 is up")
assert len(refs) >= 1
assert any("11434" in r for r in refs)
def test_127_with_port(self):
refs = _detect_local_service_refs("curl http://127.0.0.1:8080/health")
assert len(refs) >= 1
def test_check_ollama(self):
refs = _detect_local_service_refs("Check Ollama is responding")
assert len(refs) >= 1
def test_ollama_responding(self):
refs = _detect_local_service_refs("Verify Ollama responding on this machine")
assert len(refs) >= 1
def test_curl_localhost(self):
refs = _detect_local_service_refs("curl localhost and report status")
assert len(refs) >= 1
def test_ping_localhost(self):
refs = _detect_local_service_refs("ping localhost to check connectivity")
assert len(refs) >= 1
def test_no_false_positive_cloud(self):
refs = _detect_local_service_refs("Check the weather in Paris today")
assert len(refs) == 0
def test_no_false_positive_api(self):
refs = _detect_local_service_refs("Call the OpenRouter API endpoint")
assert len(refs) == 0
def test_multiple_refs(self):
refs = _detect_local_service_refs("curl localhost:11434 then ping localhost")
assert len(refs) >= 2
class TestInjectCloudContext:
"""Cloud-context warning injection."""
def test_prepends_warning(self):
prompt = "Check Ollama is responding"
result = _inject_cloud_context(prompt, ["Check Ollama"], "nous")
assert result.startswith("[SYSTEM NOTE")
assert "nous" in result
assert prompt in result
def test_preserves_original_prompt(self):
prompt = "Check Ollama at localhost:11434"
result = _inject_cloud_context(prompt, ["localhost:11434"], "openrouter")
assert prompt in result
def test_mentions_cannot_reach(self):
prompt = "curl localhost"
result = _inject_cloud_context(prompt, ["curl localhost"], "nous")
assert "CANNOT reach" in result or "cannot reach" in result
def test_suggests_local_provider(self):
prompt = "Check Ollama"
result = _inject_cloud_context(prompt, ["Check Ollama"], "nous")
assert "local" in result.lower()
class TestCloudBypassLocal:
"""Local endpoints should not trigger injection."""
def test_local_endpoint_skips(self):
# The caller checks _is_cloud before calling _detect_local_service_refs
# so this is tested at integration level. Here we verify detection
# still finds refs (the bypass is the caller\'s responsibility).
refs = _detect_local_service_refs("Check Ollama at localhost:11434")
assert len(refs) > 0 # Detection works, caller decides whether to inject