Skip to content
Merged
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
16 changes: 10 additions & 6 deletions backend/openmlr/routes/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async def _stream():
except TimeoutError:
yield ":ping\n\n"
except asyncio.CancelledError:
pass
raise
except GeneratorExit:
pass
finally:
Expand Down Expand Up @@ -150,7 +150,7 @@ async def get_conversation(
# Re-generate title if still "New conversation" and has messages
if conv.title == "New conversation" and msgs:
msg_dicts = [_msg_dict(m) for m in msgs]
asyncio.create_task(
_task = asyncio.create_task(
_auto_title(_sm(request), _bus(request), db, conv.id, conv.uuid, msg_dicts)
)

Expand Down Expand Up @@ -408,7 +408,9 @@ async def send_message(
# Title generation (still async in web process for now)
if user_count in (1, 3):
msg_dicts = await _load_messages(db, conv.id)
asyncio.create_task(_auto_title(sm, event_bus, db, conv.id, conv.uuid, msg_dicts))
_task = asyncio.create_task(
_auto_title(sm, event_bus, db, conv.id, conv.uuid, msg_dicts)
)

return {"ok": True, "job_id": job.job_id if job else None, "background": True}

Expand Down Expand Up @@ -443,11 +445,11 @@ async def send_message(
_wire_persistence(active, db, conv.id)
active._persist_wired = True

asyncio.create_task(sm.process_message(conv.id, body.message, mode=body.mode))
_task = asyncio.create_task(sm.process_message(conv.id, body.message, mode=body.mode))

if user_count in (1, 3):
msg_dicts = await _load_messages(db, conv.id)
asyncio.create_task(_auto_title(sm, event_bus, db, conv.id, conv.uuid, msg_dicts))
_task = asyncio.create_task(_auto_title(sm, event_bus, db, conv.id, conv.uuid, msg_dicts))

return {"ok": True, "background": False}

Expand Down Expand Up @@ -601,7 +603,9 @@ async def submit_approval(
if active and active.session.pending_approval:
from ..agent.loop import _handle_approval

asyncio.create_task(_handle_approval(active.session, active.tool_router, body.approvals))
_task = asyncio.create_task(
_handle_approval(active.session, active.tool_router, body.approvals)
)
return {"ok": True}


Expand Down
12 changes: 7 additions & 5 deletions backend/openmlr/routes/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ async def _authenticate_ws(token: str | None) -> User | None:

async def _cleanup_process(pid: int, master_fd: int) -> None:
"""Clean up PTY process with SIGKILL escalation to prevent zombies."""
loop = asyncio.get_event_loop()

# Close the master fd first
try:
os.close(master_fd)
await loop.run_in_executor(None, os.close, master_fd)
except OSError:
pass

Expand All @@ -103,14 +105,14 @@ async def _cleanup_process(pid: int, master_fd: int) -> None:

# Send SIGTERM and wait with timeout
try:
os.kill(pid, signal.SIGTERM)
await loop.run_in_executor(None, os.kill, pid, signal.SIGTERM)
except ProcessLookupError:
return

# Poll up to 2 seconds for graceful exit
for _ in range(20):
try:
result, _ = os.waitpid(pid, os.WNOHANG)
result, _ = await loop.run_in_executor(None, os.waitpid, pid, os.WNOHANG)
if result != 0:
return # Process exited
except ChildProcessError:
Expand All @@ -119,8 +121,8 @@ async def _cleanup_process(pid: int, master_fd: int) -> None:

# Escalate to SIGKILL
try:
os.kill(pid, signal.SIGKILL)
os.waitpid(pid, 0) # Blocking wait after SIGKILL
await loop.run_in_executor(None, os.kill, pid, signal.SIGKILL)
await loop.run_in_executor(None, os.waitpid, pid, 0) # Blocking wait after SIGKILL
except (ProcessLookupError, ChildProcessError):
pass

Expand Down
7 changes: 3 additions & 4 deletions backend/openmlr/services/event_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ async def stop_redis_bridge(self) -> None:
self._redis_bridge_task.cancel()
try:
await self._redis_bridge_task
except asyncio.CancelledError:
pass
self._redis_bridge_task = None
finally:
self._redis_bridge_task = None

@property
def subscriber_count(self) -> int:
Expand All @@ -130,6 +129,6 @@ async def sse_generator(queue: asyncio.Queue) -> AsyncGenerator[str, None]:
except TimeoutError:
yield ":ping\n\n"
except asyncio.CancelledError:
pass
raise
except GeneratorExit:
pass
2 changes: 1 addition & 1 deletion backend/openmlr/services/redis_pubsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
finally:
pass

Check warning on line 98 in backend/openmlr/services/redis_pubsub.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either remove or fill this block of code.

See more on https://sonarcloud.io/project/issues?id=xprilion_OpenMLR&issues=AZ3ZOfR5J_cg1R8QEh33&open=AZ3ZOfR5J_cg1R8QEh33&pullRequest=30
logger.info("Redis event bridge stopped")

def subscribe(self) -> asyncio.Queue:
Expand Down
4 changes: 2 additions & 2 deletions backend/openmlr/tasks/agent_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
await clear_interrupt(conversation_id)
break
except asyncio.CancelledError:
pass
raise
except Exception as e:
logger.warning(f"Interrupt poll error: {e}")

Expand Down Expand Up @@ -234,8 +234,8 @@
interrupt_task.cancel()
try:
await interrupt_task
except asyncio.CancelledError:
finally:
pass

Check warning on line 238 in backend/openmlr/tasks/agent_tasks.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either remove or fill this block of code.

See more on https://sonarcloud.io/project/issues?id=xprilion_OpenMLR&issues=AZ3ZOfVgJ_cg1R8QEh34&open=AZ3ZOfVgJ_cg1R8QEh34&pullRequest=30

# Cleanup
try:
Expand Down
9 changes: 7 additions & 2 deletions backend/openmlr/tools/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ async def _direct_exec(command: str, timeout: int, cwd: str) -> tuple[str, bool]
# ── File tools (host filesystem) ─────────────────────────


def _read_file_lines(path: Path) -> list[str]:
with open(path, encoding="utf-8", errors="replace") as f:
return f.readlines()


async def _handle_read(path: str, offset: int = 1, limit: int = 2000, **kwargs) -> tuple[str, bool]:
try:
target = Path(path).expanduser()
Expand All @@ -393,8 +398,8 @@ async def _handle_read(path: str, offset: int = 1, limit: int = 2000, **kwargs)
if not target.exists():
return f"File not found: {target}", False

with open(target, encoding="utf-8", errors="replace") as f:
all_lines = f.readlines()
loop = asyncio.get_event_loop()
all_lines = await loop.run_in_executor(None, _read_file_lines, target)

start = max(0, offset - 1)
end = start + limit
Expand Down
3 changes: 1 addition & 2 deletions backend/openmlr/tools/papers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,8 +1019,7 @@ def _extract_arxiv_id(text: str) -> str | None:

def _extract_arxiv_from_ids(ids: dict) -> str | None:
"""Extract arxiv ID from OpenAlex ids dict."""
ids.get("openalex", "")
doi = ids.get("doi", "")
doi = ids.get("openalex", "") or ids.get("doi", "")
if "arXiv" in doi:
return _extract_arxiv_id(doi)
return None
Expand Down
22 changes: 13 additions & 9 deletions backend/openmlr/tools/sandbox_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,25 +144,29 @@ async def _local_probe() -> str:
"""Quick local environment probe."""
import platform
import shutil
import subprocess

lines = [f"OS: {platform.system()} {platform.release()}"]

try:
py = subprocess.run(["python3", "--version"], capture_output=True, text=True, timeout=5)
lines.append(f"Python: {py.stdout.strip()}")
py = await asyncio.create_subprocess_exec(
"python3", "--version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, _ = await asyncio.wait_for(py.communicate(), timeout=5)
lines.append(f"Python: {stdout.decode().strip()}")
except Exception:
lines.append("Python: unknown")

try:
gpu = subprocess.run(
["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
capture_output=True,
text=True,
timeout=5,
gpu = await asyncio.create_subprocess_exec(
"nvidia-smi",
"--query-gpu=name,memory.total",
"--format=csv,noheader",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await asyncio.wait_for(gpu.communicate(), timeout=5)
if gpu.returncode == 0:
lines.append(f"GPU: {gpu.stdout.strip()}")
lines.append(f"GPU: {stdout.decode().strip()}")
else:
lines.append("GPU: not available")
except Exception:
Expand Down
10 changes: 8 additions & 2 deletions backend/openmlr/tools/workspace_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Log tool failures
"""

import asyncio
import json
import logging
from contextvars import ContextVar
Expand Down Expand Up @@ -185,8 +186,13 @@ async def _workspace_search(query: str) -> tuple[str, bool]:
try:
if os.path.getsize(fpath) > 500_000:
continue
with open(fpath, encoding="utf-8", errors="ignore") as f:
content = f.read(10000)

def read_file(path: str = fpath) -> str:
with open(path, encoding="utf-8", errors="ignore") as f:
return f.read(10000)

loop = asyncio.get_event_loop()
content = await loop.run_in_executor(None, read_file)
if query_lower in content.lower():
results.append(f"- **{rel_path}** (content match)")
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def test_roundtrip(self):
restored = ComputeCapabilities.from_dict(d)
assert restored.platform == "test"
assert restored.cpu_cores == 16
assert restored.available_ram_gb == 32.5
assert restored.available_ram_gb == pytest.approx(32.5)
assert len(restored.gpu_info) == 2
assert restored.docker_available is True

Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_yolo_mode_default(self):
assert AgentConfig().yolo_mode is False

def test_compact_threshold_ratio_default(self):
assert AgentConfig().compact_threshold_ratio == 0.90
assert AgentConfig().compact_threshold_ratio == pytest.approx(0.90)

def test_untouched_messages_default(self):
assert AgentConfig().untouched_messages == 5
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_db_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ async def test_set_user_setting_int_value(self, db_session: AsyncSession, test_u
async def test_set_user_setting_float_value(self, db_session: AsyncSession, test_user):
await ops.set_user_setting(db_session, test_user.id, "agent", "threshold", 0.85)
val = await ops.get_user_setting(db_session, test_user.id, "agent", "threshold")
assert val == 0.85
assert val == pytest.approx(0.85)

async def test_get_user_agent_settings(self, db_session: AsyncSession, test_user):
await ops.set_user_setting(db_session, test_user.id, "agent", "default_model", "claude")
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_renders_with_tools(self):
prompt = build_system_prompt(tool_specs=tools, mode="general", username="tester")
assert isinstance(prompt, str)
assert len(prompt) > 0
assert "read_file" in prompt or "read_file" in prompt
assert "read_file" in prompt

def test_renders_with_username(self):
tools = [ToolSpec(name="test_tool", description="Test", parameters={"type": "object"})]
Expand Down
6 changes: 3 additions & 3 deletions backend/tests/test_sandbox_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def test_defaults(self):
assert caps.gpu_available is False
assert caps.gpu_info == []
assert caps.installed_packages == []
assert caps.available_disk_gb == 0.0
assert caps.available_ram_gb == 0.0
assert caps.available_disk_gb == pytest.approx(0.0)
assert caps.available_ram_gb == pytest.approx(0.0)

def test_custom_values(self):
caps = ComputeCapabilities(
Expand Down Expand Up @@ -47,7 +47,7 @@ def test_defaults(self):
assert r.output == "done"
assert r.success is True
assert r.exit_code == 0
assert r.duration_seconds == 0.0
assert r.duration_seconds == pytest.approx(0.0)

def test_failure(self):
r = ExecutionResult(output="error", success=False, exit_code=1, duration_seconds=2.5)
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def test_log_experiment(self, workspace_dir):
assert filepath.exists()
data = json.loads(filepath.read_text())
assert data["name"] == "train-bert"
assert data["result"]["accuracy"] == 0.95
assert data["result"]["accuracy"] == pytest.approx(0.95)

def test_state_persistence(self, workspace_dir):
wp = WorkspacePersistence(workspace_dir)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/ModelModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect, useMemo, useCallback } from 'react';
import { Search, ChevronDown, Check, X, Filter, Save } from 'lucide-react';
import { api } from '../api';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/ProjectManageModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useRef, useEffect } from 'react';
import { X, FolderOpen, Pencil, Trash2, Check, Layers } from 'lucide-react';
import { api } from '../api';
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { useState } from 'react';
import { X, FolderPlus } from 'lucide-react';
import { api } from '../api';
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/RightPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { useState } from 'react';
import {
Circle,
Expand Down Expand Up @@ -42,8 +43,8 @@ function SearchBudgetDialog({ currentMax, onSave, onClose }: { currentMax: numbe
const [saving, setSaving] = useState(false);

const handleSave = async () => {
const num = parseInt(value, 10);
if (isNaN(num) || num < 1) return;
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1) return;
setSaving(true);
try {
await api.updateSetting('agent', 'paper_search_budget', num);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { Link, Outlet, useLocation } from 'react-router-dom';
import { ArrowLeft, Key, Bot, Server, Cpu, PenTool } from 'lucide-react';

Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { useEffect, useRef, useState, useCallback } from 'react';
import {
Terminal as TerminalIcon,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/TodoReviewDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import { useState } from 'react';
import { Check, X, Plus, Trash2, GripVertical } from 'lucide-react';
import { api } from '../api';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/AddKeyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect } from 'react';
import { X, Upload, KeyRound } from 'lucide-react';

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/AddNodeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect } from 'react';
import { X, Server, Monitor, Cloud, TestTube } from 'lucide-react';
import { api } from '../../api';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/AgentSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect, useCallback } from 'react';
import { api } from '../../api';

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/McpSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect, useCallback } from 'react';
import { api } from '../../api';

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/ProvidersSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect, useCallback, useMemo } from 'react';
import { api } from '../../api';
import type { Provider } from '../../types';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/settings/WritingSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


import { useState, useEffect, useCallback } from 'react';
import { api } from '../../api';

Expand Down
Loading