Compare commits

...
Author SHA1 Message Date
Elliot Slusky bcdbf13d02 test(tools): cover eager deep-research registration 2026-08-09 22:59:45 -07:00
Ari 3dc621618f fix(tools): eager-import scan_chunks and knowledge_sql at package load
Every other built-in tool is imported here specifically to fire its
@ToolRegistry.register() decorator at package-load time; these two
were missing, so their test_registered tests only passed when some
unrelated test (via agent_manager_routes.py, channels_cmd.py, or
deep_research_setup_cmd.py) happened to import the module first in
the same process. Under pytest-xdist that's worker-distribution
dependent, so adding an unrelated test file could flip either test
from pass to fail.
2026-08-09 22:59:45 -07:00
github-actions[bot] fd0b60fefc chore: update clone traffic data [skip ci] 2026-08-09 06:51:50 +00:00
github-actions[bot] 95a9857984 chore: update clone traffic data [skip ci] 2026-08-08 06:46:50 +00:00
github-actions[bot] f9c89308fc chore: update clone traffic data [skip ci] 2026-08-07 07:10:58 +00:00
Elliot Slusky 65d08e9d94 Fix proactive cron reconciliation and notifications 2026-08-06 13:40:00 -07:00
Loma 45717780fa Fix proactive agent cron duplicating on every server restart
register_cron() ran unconditionally on every 'jarvis serve' startup and
create_task() persists to scheduler.db, so each restart added another
copy of the daily proactive cron. On a real install 68 duplicates
accumulated; when due they fired back-to-back and monopolized the
single-slot local inference queue, stalling interactive chat.

register_cron() is now idempotent: an existing active task is reused and
surplus duplicates are cancelled. Also stop the notification channel
from calling connect() — a second getUpdates poll loop on the same bot
token makes Telegram return Conflict and kills the main listener.
2026-08-06 13:40:00 -07:00
goatoush 9da7c30880 Fixing text wrap for agent creator tool selector (#655) 2026-08-06 11:23:27 -07:00
github-actions[bot] 98e791f258 chore: update clone traffic data [skip ci] 2026-08-06 08:31:13 +00:00
Elliot Slusky b9e0928aef fix(telemetry): record cloud inference cost (#704) 2026-08-05 20:51:06 -07:00
9 changed files with 289 additions and 18 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "Git Clones",
"message": "181,942",
"message": "183,817",
"color": "green",
"namedLogo": "git"
}
+6 -3
View File
@@ -1,6 +1,6 @@
{
"total_clones": 181942,
"last_updated": "2026-08-05T08:31:05Z",
"total_clones": 183817,
"last_updated": "2026-08-09T06:51:49Z",
"daily": {
"2026-03-27": 2189,
"2026-03-28": 1874,
@@ -132,6 +132,9 @@
"2026-08-01": 567,
"2026-08-02": 1248,
"2026-08-03": 724,
"2026-08-04": 708
"2026-08-04": 708,
"2026-08-05": 647,
"2026-08-06": 604,
"2026-08-07": 624
}
}
+3 -2
View File
@@ -574,7 +574,7 @@ function ToolsPicker({
</div>
{/* Live description strip */}
<div
className="flex items-center gap-2 px-2.5 py-1.5"
className="flex items-start gap-2 px-2.5 py-1.5"
style={{
borderTop: '1px solid var(--color-border)',
background: 'var(--color-bg)',
@@ -608,10 +608,11 @@ function ToolsPicker({
</span>
)}
<span
className="truncate"
className="min-w-0 whitespace-normal break-words"
style={{
flex: 1,
color: 'var(--color-text-tertiary)',
lineHeight: 1.4,
}}
>
{hovered ? `${hint}` : hint}
+99 -12
View File
@@ -37,6 +37,7 @@ called from your app startup:
from __future__ import annotations
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
@@ -56,6 +57,15 @@ from openjarvis.tools.approval_store import (
)
from openjarvis.tools.proactive_tools import get_store
logger = logging.getLogger(__name__)
_PROACTIVE_CRON_PROMPT = (
"Run the proactive agent: collect overnight data, execute approved actions, "
"notify pending approvals."
)
_PROACTIVE_TASK_KEY = "proactive-daily"
_PROACTIVE_TASK_KEY_FIELD = "openjarvis_task_key"
_SYSTEM_PROMPT = """You are a proactive personal assistant agent. You have already collected
data from the user's connected sources (email, messages, calendar). Your job is to:
@@ -252,14 +262,31 @@ def _build_notification_channel(channel_spec: str) -> Optional[Any]:
if ChannelRegistry.contains(channel_type):
channel_cls = ChannelRegistry.get(channel_type)
instance = channel_cls()
# Load credentials from config so the channel uses bot_token from
# config.toml rather than falling back to a bare env var.
try:
instance.connect()
from openjarvis.core.config import load_config
from openjarvis.system._channel_kwargs import build_channel_kwargs
_cfg = load_config()
_kwargs = build_channel_kwargs(_cfg.channel, channel_type)
except Exception:
pass
_kwargs = {}
instance = channel_cls(**_kwargs)
# Telegram.send() is self-contained, while connect() starts a
# getUpdates loop. A second loop for the same bot token conflicts
# with the server's main listener. Other channel implementations
# may initialize resources required by send() in connect(), so keep
# their established lifecycle intact.
if channel_type != "telegram":
instance.connect()
return instance
except Exception:
pass
logger.warning(
"Failed to build proactive notification channel %s",
channel_type,
exc_info=True,
)
return None
@@ -299,6 +326,7 @@ class ProactiveAgent(ToolUsingAgent):
self._notification_channel_id
)
self._notification_channel = notification_channel
self._notification_destination = self._notification_channel_id.partition(":")[2]
from openjarvis.tools.channel_tools import ChannelSendTool
from openjarvis.tools.digest_collect import DigestCollectTool
@@ -484,13 +512,13 @@ class ProactiveAgent(ToolUsingAgent):
# --- Step 5: Build and send notification ---
notification = self._build_notification(executed_results, pending_actions)
if notification and self._notification_channel_id:
if notification and self._notification_destination:
send_call = ToolCall(
id="proactive-notify-1",
name="channel_send",
arguments=json.dumps(
{
"channel": self._notification_channel_id,
"channel": self._notification_destination,
"content": notification,
}
),
@@ -592,15 +620,74 @@ def register_cron(
hours_back = hours_back or 24
timezone = timezone or "America/Los_Angeles"
metadata = {
"notification_channel_id": notification_channel_id,
"hours_back": hours_back,
"timezone": timezone,
_PROACTIVE_TASK_KEY_FIELD: _PROACTIVE_TASK_KEY,
}
# Match the stable key for tasks created by this version and the historical
# agent+prompt signature so existing installations are migrated on startup.
existing = [
task
for task in scheduler.list_tasks()
if task.status in {"active", "paused"}
and task.agent == "proactive"
and (
task.metadata.get(_PROACTIVE_TASK_KEY_FIELD) == _PROACTIVE_TASK_KEY
or (task.prompt == _PROACTIVE_CRON_PROMPT and task.schedule_type == "cron")
)
]
# A scheduler pause is an explicit user choice and must survive restart.
# Keep one deterministically and remove any active or paused duplicates.
paused = [task for task in existing if task.status == "paused"]
if paused:
keep = min(paused, key=lambda task: task.id)
_cancel_proactive_duplicates(scheduler, existing, keep=keep)
return keep
matching = [
task
for task in existing
if task.prompt == _PROACTIVE_CRON_PROMPT
and task.schedule_type == "cron"
and task.schedule_value == cron_expr
and task.context_mode == "isolated"
and task.metadata == metadata
]
if matching:
keep = min(matching, key=lambda task: task.id)
_cancel_proactive_duplicates(scheduler, existing, keep=keep)
return keep
# Configuration changed. Replace stale active tasks so the schedule and
# notification settings from config.toml take effect on this startup.
_cancel_proactive_duplicates(scheduler, existing)
return scheduler.create_task(
prompt="Run the proactive agent: collect overnight data, execute approved actions, notify pending approvals.",
prompt=_PROACTIVE_CRON_PROMPT,
schedule_type="cron",
schedule_value=cron_expr,
agent="proactive",
context_mode="isolated",
metadata={
"notification_channel_id": notification_channel_id,
"hours_back": hours_back,
"timezone": timezone,
},
metadata=metadata,
)
def _cancel_proactive_duplicates(
scheduler: Any, tasks: List[Any], *, keep: Optional[Any] = None
) -> None:
"""Cancel managed proactive tasks other than *keep*."""
for task in tasks:
if keep is not None and task.id == keep.id:
continue
try:
scheduler.cancel_task(task.id)
except Exception:
logger.warning(
"Failed to cancel duplicate proactive task %s",
task.id,
exc_info=True,
)
@@ -212,6 +212,7 @@ class InstrumentedEngine(InferenceEngine):
completion_tokens=completion_tokens,
total_tokens=prompt_tok + completion_tokens,
latency_seconds=latency,
cost_usd=result.get("cost_usd", 0.0),
ttft=ttft,
throughput_tok_per_sec=throughput,
energy_per_output_token_joules=energy_per_output_token,
+10
View File
@@ -142,4 +142,14 @@ try:
except ImportError:
pass
try:
import openjarvis.tools.scan_chunks # noqa: F401
except ImportError:
pass
try:
import openjarvis.tools.knowledge_sql # noqa: F401
except ImportError:
pass
__all__ = ["BaseTool", "ToolExecutor", "ToolSpec"]
+131
View File
@@ -0,0 +1,131 @@
"""Regression tests for proactive scheduling and notification setup."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from openjarvis.agents.proactive_agent import (
_PROACTIVE_CRON_PROMPT,
_build_notification_channel,
register_cron,
)
from openjarvis.core.registry import ChannelRegistry
from openjarvis.scheduler.scheduler import TaskScheduler
from openjarvis.scheduler.store import SchedulerStore
@pytest.fixture()
def scheduler(tmp_path):
store = SchedulerStore(tmp_path / "scheduler.db")
scheduler = TaskScheduler(store)
yield scheduler
scheduler.stop()
store.close()
def _register(scheduler, *, schedule="0 5 * * *", channel="telegram:123"):
return register_cron(
scheduler,
notification_channel_id=channel,
cron_expr=schedule,
hours_back=24,
timezone="UTC",
)
class TestRegisterCron:
def test_reuses_exact_task_and_cancels_duplicates(self, scheduler):
first = _register(scheduler)
duplicate = scheduler.create_task(
_PROACTIVE_CRON_PROMPT,
"cron",
"0 5 * * *",
agent="proactive",
metadata=first.metadata,
)
returned = _register(scheduler)
assert returned.id in {first.id, duplicate.id}
assert [task.id for task in scheduler.list_tasks(status="active")] == [
returned.id
]
cancelled_id = scheduler.list_tasks(status="cancelled")[0].id
assert cancelled_id == ({first.id, duplicate.id} - {returned.id}).pop()
def test_replaces_task_when_configuration_changes(self, scheduler):
old = _register(scheduler, schedule="0 5 * * *", channel="telegram:old")
new = _register(scheduler, schedule="0 7 * * *", channel="telegram:new")
assert new.id != old.id
assert new.schedule_value == "0 7 * * *"
assert new.metadata["notification_channel_id"] == "telegram:new"
assert scheduler.list_tasks(status="cancelled")[0].id == old.id
def test_preserves_pause_across_restart(self, scheduler):
paused = _register(scheduler)
scheduler.pause_task(paused.id)
returned = _register(scheduler, schedule="0 7 * * *")
assert returned.id == paused.id
assert returned.status == "paused"
assert scheduler.list_tasks(status="active") == []
def test_migrates_legacy_tasks_without_stable_key(self, scheduler):
legacy = scheduler.create_task(
_PROACTIVE_CRON_PROMPT,
"cron",
"0 5 * * *",
agent="proactive",
metadata={
"notification_channel_id": "telegram:123",
"hours_back": 24,
"timezone": "UTC",
},
)
current = _register(scheduler)
assert current.id != legacy.id
assert current.metadata["openjarvis_task_key"] == "proactive-daily"
assert scheduler.list_tasks(status="cancelled")[0].id == legacy.id
class TestNotificationChannel:
def test_telegram_is_configured_without_starting_polling(self):
class FakeTelegram:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.connect = MagicMock()
config = MagicMock()
with (
patch.object(ChannelRegistry, "contains", return_value=True),
patch.object(ChannelRegistry, "get", return_value=FakeTelegram),
patch("openjarvis.core.config.load_config", return_value=config),
patch(
"openjarvis.system._channel_kwargs.build_channel_kwargs",
return_value={"bot_token": "configured-token"},
),
):
channel = _build_notification_channel("telegram:123")
assert channel.kwargs == {"bot_token": "configured-token"}
channel.connect.assert_not_called()
def test_non_telegram_channel_keeps_connect_lifecycle(self):
class FakeChannel:
def __init__(self, **kwargs):
self.connect = MagicMock()
with (
patch.object(ChannelRegistry, "contains", return_value=True),
patch.object(ChannelRegistry, "get", return_value=FakeChannel),
):
channel = _build_notification_channel("twilio:15551234567")
channel.connect.assert_called_once_with()
@@ -71,6 +71,17 @@ class TestInstrumentedEngine:
assert record.prompt_tokens == 10
assert record.completion_tokens == 5
def test_generate_records_cost(self, mock_engine, bus):
mock_engine.generate.return_value["cost_usd"] = 0.0015
ie = InstrumentedEngine(mock_engine, bus)
messages = [Message(role=Role.USER, content="Hi")]
ie.generate(messages, model="test")
event = next(
e for e in bus.history if e.event_type == EventType.TELEMETRY_RECORD
)
assert event.data["record"].cost_usd == pytest.approx(0.0015)
def test_list_models_delegates(self, mock_engine, bus):
ie = InstrumentedEngine(mock_engine, bus)
assert ie.list_models() == ["test-model"]
+27
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import importlib
import subprocess
import sys
from openjarvis.core.registry import ToolRegistry
@@ -68,6 +69,10 @@ EXPECTED_TOOLS = {
"kg_add_relation",
"kg_query",
"kg_neighbors",
# knowledge_sql.py
"knowledge_sql",
# scan_chunks.py
"scan_chunks",
}
@@ -100,3 +105,25 @@ def test_all_builtin_tools_registered():
assert not missing, (
f"Tools not registered (missing import in __init__.py?): {sorted(missing)}"
)
def test_package_import_registers_deep_research_tools():
"""Registration must not depend on another module being imported first."""
result = subprocess.run(
[
sys.executable,
"-c",
(
"import openjarvis.tools; "
"from openjarvis.core.registry import ToolRegistry; "
"expected = {'knowledge_sql', 'scan_chunks'}; "
"missing = expected - set(ToolRegistry.keys()); "
"assert not missing, f'Missing tools: {sorted(missing)}'"
),
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr