mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] backend: leading-_ -> p_/P_/public for class methods + module conditional-block vars + _ReadyServer class + _pending_oauth import; promote cross-file (search_ddg)
This commit is contained in:
@@ -394,8 +394,8 @@ async def subscriptions_connect(body: dict):
|
||||
result = await start_oauth(provider)
|
||||
|
||||
if result.get("flow") == "authorization_code" and result.get("state"):
|
||||
from backend.main import _pending_oauth
|
||||
_pending_oauth[result["state"]] = {
|
||||
from backend.main import p_pending_oauth
|
||||
p_pending_oauth[result["state"]] = {
|
||||
"provider": provider,
|
||||
"code_verifier": result.get("code_verifier", ""),
|
||||
"redirect_uri": result.get("redirect_uri", ""),
|
||||
|
||||
@@ -44,7 +44,7 @@ class SeqLogStore:
|
||||
except Exception:
|
||||
logger.warning("seq_log: failed to create persist dir %s", persist_dir)
|
||||
|
||||
async def _get_or_create(self, session_id: str) -> p_SessionSeqLog:
|
||||
async def p_get_or_create(self, session_id: str) -> p_SessionSeqLog:
|
||||
log = self.per_session.get(session_id)
|
||||
if log is not None:
|
||||
return log
|
||||
@@ -55,7 +55,7 @@ class SeqLogStore:
|
||||
self.per_session[session_id] = log
|
||||
return log
|
||||
|
||||
def _peek(self, session_id: str) -> Optional[p_SessionSeqLog]:
|
||||
def p_peek(self, session_id: str) -> Optional[p_SessionSeqLog]:
|
||||
return self.per_session.get(session_id)
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -63,7 +63,7 @@ class SeqLogStore:
|
||||
self, session_id: str, event: str, data: dict
|
||||
) -> AsyncIterator[tuple[int, str]]:
|
||||
"""Atomically assign seq, buffer, and yield (seq, payload); caller's send must happen inside the with-block."""
|
||||
log = await self._get_or_create(session_id)
|
||||
log = await self.p_get_or_create(session_id)
|
||||
async with log.lock:
|
||||
log.seq += 1
|
||||
seq = log.seq
|
||||
@@ -81,7 +81,7 @@ class SeqLogStore:
|
||||
self, session_id: str, last_seq: int
|
||||
) -> tuple[Optional[int], Optional[int], list[str]]:
|
||||
"""Return (oldest_buffered_seq, newest_buffered_seq, events)."""
|
||||
log = self._peek(session_id)
|
||||
log = self.p_peek(session_id)
|
||||
if log is None:
|
||||
return (None, None, [])
|
||||
# asyncio is single-threaded; deque list() is safe vs concurrent append/eviction. No lock needed for read.
|
||||
@@ -95,10 +95,10 @@ class SeqLogStore:
|
||||
|
||||
def current_seq(self, session_id: str) -> int:
|
||||
"""Last assigned seq, or 0 if no log exists for the session."""
|
||||
log = self._peek(session_id)
|
||||
log = self.p_peek(session_id)
|
||||
return log.seq if log else 0
|
||||
|
||||
def _terminal_path(self, session_id: str) -> Optional[str]:
|
||||
def p_terminal_path(self, session_id: str) -> Optional[str]:
|
||||
if not self.p_persist_dir:
|
||||
return None
|
||||
# Session ids are uuid4 hex; sanitize anyway against path traversal.
|
||||
@@ -109,7 +109,7 @@ class SeqLogStore:
|
||||
|
||||
def persist_terminal(self, session_id: str, payload_str: str) -> None:
|
||||
"""Atomic write of a terminal event for post-restart clients; best-effort, never blocks broadcast."""
|
||||
path = self._terminal_path(session_id)
|
||||
path = self.p_terminal_path(session_id)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
@@ -123,7 +123,7 @@ class SeqLogStore:
|
||||
)
|
||||
|
||||
def load_terminal(self, session_id: str) -> Optional[str]:
|
||||
path = self._terminal_path(session_id)
|
||||
path = self.p_terminal_path(session_id)
|
||||
if not path or not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
@@ -135,7 +135,7 @@ class SeqLogStore:
|
||||
def clear(self, session_id: str) -> None:
|
||||
"""Drop in-memory log and persisted terminal; for full deletion only, closed-but-retained sessions keep it."""
|
||||
self.per_session.pop(session_id, None)
|
||||
path = self._terminal_path(session_id)
|
||||
path = self.p_terminal_path(session_id)
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
|
||||
@@ -135,8 +135,8 @@ class ConnectionManager:
|
||||
# wipes pending_futures, which is correct because
|
||||
# reconcile_on_startup also marks waiting_approval sessions as
|
||||
# stopped so there's nothing to answer anyway.
|
||||
events = self._filter_stale_approvals(events)
|
||||
events = self._strip_replayed_closes(events)
|
||||
events = self.p_filter_stale_approvals(events)
|
||||
events = self.p_strip_replayed_closes(events)
|
||||
for s in events:
|
||||
try:
|
||||
await websocket.send_text(s)
|
||||
@@ -164,7 +164,7 @@ class ConnectionManager:
|
||||
"current_seq": newest if newest is not None else 0,
|
||||
}
|
||||
|
||||
def _strip_replayed_closes(self, events: list[str]) -> list[str]:
|
||||
def p_strip_replayed_closes(self, events: list[str]) -> list[str]:
|
||||
"""Drop `agent:closed` events from a replay buffer.
|
||||
|
||||
agent:closed is a transition event ("session JUST closed") whose
|
||||
@@ -188,7 +188,7 @@ class ConnectionManager:
|
||||
out.append(payload_str)
|
||||
return out
|
||||
|
||||
def _filter_stale_approvals(self, events: list[str]) -> list[str]:
|
||||
def p_filter_stale_approvals(self, events: list[str]) -> list[str]:
|
||||
"""Return events minus any `agent:approval_request` whose request_id
|
||||
is no longer in pending_futures. JSON parse is per-event but replay
|
||||
only runs on (re)connect, so it isn't a hot path.
|
||||
|
||||
@@ -16,7 +16,7 @@ P_OR_MODELS_TTL_OK = 3600.0
|
||||
P_OR_MODELS_TTL_FAIL = 30.0
|
||||
p_or_models_cache: dict = {"models": None, "fetched_at": 0.0, "ok": False}
|
||||
|
||||
_9router_cache: dict = {"available": None, "checked_at": 0}
|
||||
p_9router_cache: dict = {"available": None, "checked_at": 0}
|
||||
|
||||
|
||||
# Per-model published pricing in $/1M tokens (input, output) for direct
|
||||
|
||||
@@ -139,7 +139,7 @@ class WebSearchTool(BaseTool):
|
||||
num_results: int = input_data.get("num_results", 5)
|
||||
|
||||
try:
|
||||
results = await self._search_ddg(query, num_results)
|
||||
results = await self.search_ddg(query, num_results)
|
||||
if not results:
|
||||
return [{"type": "text", "text": f"No search results found for: {query}"}]
|
||||
return [{"type": "text", "text": results}]
|
||||
@@ -152,7 +152,7 @@ class WebSearchTool(BaseTool):
|
||||
return [{"type": "text", "text": f"Web search error: {exc}"}]
|
||||
|
||||
@staticmethod
|
||||
async def _search_ddg(query: str, num_results: int) -> str:
|
||||
async def search_ddg(query: str, num_results: int) -> str:
|
||||
"""Query DuckDuckGo HTML endpoint and parse results."""
|
||||
async with httpx.AsyncClient(
|
||||
timeout=P_HTTP_TIMEOUT,
|
||||
|
||||
@@ -34,7 +34,7 @@ class Output(BaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate_flat_fields(cls, data: Any) -> Any:
|
||||
def p_migrate_flat_fields(cls, data: Any) -> Any:
|
||||
"""Migrate legacy frontend_code/backend_code fields into the files dict."""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
@@ -91,7 +91,7 @@ class OutputCreate(BaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate_flat_fields(cls, data: Any) -> Any:
|
||||
def p_migrate_flat_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if "files" not in data or not data["files"]:
|
||||
@@ -121,7 +121,7 @@ class OutputUpdate(BaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate_flat_fields(cls, data: Any) -> Any:
|
||||
def p_migrate_flat_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if "files" not in data:
|
||||
@@ -186,7 +186,7 @@ class WorkspaceSeedRequest(BaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _migrate_flat_fields(cls, data: Any) -> Any:
|
||||
def p_migrate_flat_fields(cls, data: Any) -> Any:
|
||||
"""Accept legacy frontend_code/backend_code/schema_json fields."""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
@@ -82,7 +82,7 @@ class AppRuntime:
|
||||
self.frontend_port: Optional[int] = None
|
||||
# New-mode only: flips True once something is actually listening
|
||||
# on frontend_port (we kick off a background poll task in
|
||||
# _start_new_mode). frontend_url returns null until this flips,
|
||||
# p_start_new_mode). frontend_url returns null until this flips,
|
||||
# so the preview pane doesn't try to navigate to an unbound port
|
||||
# and show a "Site can't be reached" error mid-npm-install.
|
||||
self.p_frontend_ready: bool = False
|
||||
@@ -142,7 +142,7 @@ class AppRuntime:
|
||||
@property
|
||||
def frontend_url(self) -> Optional[str]:
|
||||
# Gated on `_frontend_ready` (set by the background bind-poll
|
||||
# task in _start_new_mode) so the preview pane only switches
|
||||
# task in p_start_new_mode) so the preview pane only switches
|
||||
# over once Vite is actually accepting connections. Without
|
||||
# this, the editor flashes a "Site can't be reached" error
|
||||
# while `npm install` is running.
|
||||
@@ -189,10 +189,10 @@ class AppRuntime:
|
||||
# vite emits "frontend ready" (or its 180s timeout
|
||||
# fires), which is the moment the next workspace can
|
||||
# start its own vite without competing for the same
|
||||
# CPU. See `_await_frontend_bind` for the release.
|
||||
# CPU. See `p_await_frontend_bind` for the release.
|
||||
await p_vite_boot_lock.acquire()
|
||||
try:
|
||||
ok = await self._start_new_mode()
|
||||
ok = await self.p_start_new_mode()
|
||||
if not ok:
|
||||
# Spawn failed before the bind-poll task was
|
||||
# created; release synchronously so we don't
|
||||
@@ -202,9 +202,9 @@ class AppRuntime:
|
||||
except Exception:
|
||||
p_vite_boot_lock.release()
|
||||
raise
|
||||
return await self._start_old_mode()
|
||||
return await self.p_start_old_mode()
|
||||
|
||||
async def _start_new_mode(self) -> bool:
|
||||
async def p_start_new_mode(self) -> bool:
|
||||
env_path = os.path.join(self.workspace_path, ".env")
|
||||
fp_raw = read_env_value(env_path, "FRONTEND_PORT")
|
||||
bp_raw = read_env_value(env_path, "BACKEND_PORT")
|
||||
@@ -222,7 +222,7 @@ class AppRuntime:
|
||||
# .env so the bash run.sh subprocess reads the new port.
|
||||
if self.frontend_port and not is_port_free(self.frontend_port):
|
||||
new_port = find_free_port()
|
||||
self._broadcast(LogLine(
|
||||
self.p_broadcast(LogLine(
|
||||
"runtime",
|
||||
f"[runtime] persisted FRONTEND_PORT {self.frontend_port} is in use; reallocating to {new_port}",
|
||||
))
|
||||
@@ -240,7 +240,7 @@ class AppRuntime:
|
||||
# from a prior session would otherwise block the new spawn.
|
||||
if self.port and not is_port_free(self.port):
|
||||
new_port = find_free_port()
|
||||
self._broadcast(LogLine(
|
||||
self.p_broadcast(LogLine(
|
||||
"runtime",
|
||||
f"[runtime] persisted BACKEND_PORT {self.port} is in use; reallocating to {new_port}",
|
||||
))
|
||||
@@ -249,7 +249,7 @@ class AppRuntime:
|
||||
else:
|
||||
self.port = None
|
||||
|
||||
env = self._spawn_env_base()
|
||||
env = self.p_spawn_env_base()
|
||||
# bash run.sh reads .env itself; we don't need to set
|
||||
# FRONTEND_PORT / BACKEND_PORT here. We DO export the install
|
||||
# paths so the template's `backend/run.sh` can find our
|
||||
@@ -264,7 +264,7 @@ class AppRuntime:
|
||||
env["OPENSWARM_DEBUGGER_PATH"] = DEBUGGER_PATH
|
||||
env["OPENSWARM_TEMPLATE_BACKEND_PATH"] = TEMPLATE_BACKEND_PATH
|
||||
|
||||
cmd, spawn_cwd, launch_desc = self._resolve_launch(env)
|
||||
cmd, spawn_cwd, launch_desc = self.p_resolve_launch(env)
|
||||
try:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
@@ -276,23 +276,23 @@ class AppRuntime:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("failed to start new-mode runtime for %s", self.workspace_id)
|
||||
self._broadcast(LogLine("runtime", f"[runtime] failed to start: {e}"))
|
||||
self.p_broadcast(LogLine("runtime", f"[runtime] failed to start: {e}"))
|
||||
self.frontend_port = None
|
||||
self.port = None
|
||||
self.process = None
|
||||
return False
|
||||
backend_note = f" + backend on {self.port}" if self.port else ""
|
||||
self._broadcast(LogLine("runtime", f"[runtime] {launch_desc} started; frontend on {self.frontend_port}{backend_note} (pid {self.process.pid})"))
|
||||
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self._await_exit())
|
||||
self.p_broadcast(LogLine("runtime", f"[runtime] {launch_desc} started; frontend on {self.frontend_port}{backend_note} (pid {self.process.pid})"))
|
||||
self.p_stdout_task = asyncio.create_task(self.p_pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self.p_pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self.p_await_exit())
|
||||
# Kick off the port-bind poller so frontend_url flips on once
|
||||
# Vite is actually accepting connections.
|
||||
self.p_frontend_ready = False
|
||||
self.p_frontend_ready_task = asyncio.create_task(self._await_frontend_bind())
|
||||
self.p_frontend_ready_task = asyncio.create_task(self.p_await_frontend_bind())
|
||||
return True
|
||||
|
||||
def _resolve_launch(self, env: dict) -> tuple[list[str], str, str]:
|
||||
def p_resolve_launch(self, env: dict) -> tuple[list[str], str, str]:
|
||||
"""Pick the new-mode launch command.
|
||||
|
||||
Default is `bash run.sh` at the workspace root, which handles both
|
||||
@@ -319,7 +319,7 @@ class AppRuntime:
|
||||
)
|
||||
return [p_resolve_bash(), "run.sh"], self.workspace_path, "bash run.sh"
|
||||
|
||||
async def _await_frontend_bind(self) -> None:
|
||||
async def p_await_frontend_bind(self) -> None:
|
||||
"""Poll `frontend_port` every FRONTEND_BIND_POLL_INTERVAL until
|
||||
something binds (Vite dev server) or we hit the timeout. Emits a
|
||||
`[runtime]` log line on success/failure so the Terminal pane
|
||||
@@ -369,7 +369,7 @@ class AppRuntime:
|
||||
except Exception:
|
||||
pass
|
||||
self.p_frontend_ready = True
|
||||
self._broadcast(LogLine(
|
||||
self.p_broadcast(LogLine(
|
||||
"runtime",
|
||||
f"[runtime] frontend ready at http://127.0.0.1:{port}/",
|
||||
))
|
||||
@@ -384,7 +384,7 @@ class AppRuntime:
|
||||
await asyncio.sleep(FRONTEND_BIND_POLL_INTERVAL)
|
||||
# Timed out; keep the runtime up (Terminal might show useful
|
||||
# errors) but surface why the preview never appeared.
|
||||
self._broadcast(LogLine(
|
||||
self.p_broadcast(LogLine(
|
||||
"runtime",
|
||||
f"[runtime] frontend did NOT bind on port {port} after "
|
||||
f"{FRONTEND_BIND_TIMEOUT_SECONDS}s; check the Terminal "
|
||||
@@ -397,12 +397,12 @@ class AppRuntime:
|
||||
# already released.
|
||||
p_release_boot_lock()
|
||||
|
||||
async def _start_old_mode(self) -> bool:
|
||||
async def p_start_old_mode(self) -> bool:
|
||||
if not self.has_backend_file:
|
||||
self.port = None
|
||||
return False
|
||||
self.port = find_free_port()
|
||||
env = self._spawn_env_base()
|
||||
env = self.p_spawn_env_base()
|
||||
env["PORT"] = str(self.port)
|
||||
env["BACKEND_PORT"] = str(self.port) # alias; both common names work
|
||||
try:
|
||||
@@ -419,17 +419,17 @@ class AppRuntime:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("failed to start backend for %s", self.workspace_id)
|
||||
self._broadcast(LogLine("runtime", f"[runtime] failed to start: {e}"))
|
||||
self.p_broadcast(LogLine("runtime", f"[runtime] failed to start: {e}"))
|
||||
self.port = None
|
||||
self.process = None
|
||||
return False
|
||||
self._broadcast(LogLine("runtime", f"[runtime] backend started on port {self.port} (pid {self.process.pid})"))
|
||||
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self._await_exit())
|
||||
self.p_broadcast(LogLine("runtime", f"[runtime] backend started on port {self.port} (pid {self.process.pid})"))
|
||||
self.p_stdout_task = asyncio.create_task(self.p_pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self.p_pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self.p_await_exit())
|
||||
return True
|
||||
|
||||
def _spawn_env_base(self) -> dict[str, str]:
|
||||
def p_spawn_env_base(self) -> dict[str, str]:
|
||||
"""Inherited env minus the install token. Backend.py can hit our
|
||||
REST API back via its own creds if it really needs to, but it
|
||||
shouldn't inherit the host process's token by default."""
|
||||
@@ -492,7 +492,7 @@ class AppRuntime:
|
||||
|
||||
return p_unsub
|
||||
|
||||
def _broadcast(self, line: LogLine) -> None:
|
||||
def p_broadcast(self, line: LogLine) -> None:
|
||||
self.log_buffer.append(line)
|
||||
# Snapshot subscribers; they can self-remove during dispatch.
|
||||
for cb in list(self.p_subscribers):
|
||||
@@ -501,7 +501,7 @@ class AppRuntime:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _maybe_capture_error(self, text: str) -> None:
|
||||
def p_maybe_capture_error(self, text: str) -> None:
|
||||
if ERROR_PATTERNS.search(text):
|
||||
self.recent_errors.append(text.rstrip())
|
||||
|
||||
@@ -512,7 +512,7 @@ class AppRuntime:
|
||||
idx = text.index("[openswarm:app-error]") + len("[openswarm:app-error]")
|
||||
self.set_render_error(text[idx:].strip())
|
||||
|
||||
async def _pipe_stream(self, stream: Optional[asyncio.StreamReader], name: str) -> None:
|
||||
async def p_pipe_stream(self, stream: Optional[asyncio.StreamReader], name: str) -> None:
|
||||
if stream is None:
|
||||
return
|
||||
try:
|
||||
@@ -522,14 +522,14 @@ class AppRuntime:
|
||||
break
|
||||
text = raw.decode(errors="replace").rstrip("\r\n")
|
||||
if text:
|
||||
self._broadcast(LogLine(name, text))
|
||||
self.p_broadcast(LogLine(name, text))
|
||||
if name == "stderr" or name == "stdout":
|
||||
self._maybe_capture_error(text)
|
||||
self.p_maybe_capture_error(text)
|
||||
self.p_maybe_capture_render_beacon(text)
|
||||
except Exception:
|
||||
logger.exception("log pipe error (%s) for %s", name, self.workspace_id)
|
||||
|
||||
async def _await_exit(self) -> None:
|
||||
async def p_await_exit(self) -> None:
|
||||
if not self.process:
|
||||
return
|
||||
rc = await self.process.wait()
|
||||
@@ -537,7 +537,7 @@ class AppRuntime:
|
||||
# otherwise frontend_url keeps advertising a dead port and the preview
|
||||
# navigates into ERR_FAILED. stop() already does this for clean stops.
|
||||
self.p_frontend_ready = False
|
||||
self._broadcast(LogLine("runtime", f"[runtime] backend exited with code {rc}"))
|
||||
self.p_broadcast(LogLine("runtime", f"[runtime] backend exited with code {rc}"))
|
||||
|
||||
|
||||
class AppRuntimeManager:
|
||||
|
||||
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
p_pulse_task: asyncio.Task | None = None
|
||||
p_drain_task: asyncio.Task | None = None
|
||||
_9r_start_task: asyncio.Task | None = None
|
||||
p_9r_start_task: asyncio.Task | None = None
|
||||
|
||||
p_last_9r_cost: float | None = None
|
||||
p_last_9r_prompt_tokens: int | None = None
|
||||
@@ -122,7 +122,7 @@ async def p_drain_loop():
|
||||
|
||||
@asynccontextmanager
|
||||
async def service_lifespan():
|
||||
global p_pulse_task, p_drain_task, _9r_start_task
|
||||
global p_pulse_task, p_drain_task, p_9r_start_task
|
||||
|
||||
try:
|
||||
from backend.apps.settings.settings import load_settings, save_settings
|
||||
@@ -200,7 +200,7 @@ async def service_lifespan():
|
||||
# user sends an agent message, and the dispatch path calls ensure_running()
|
||||
# itself (now serialized, so no double-spawn), so the first message waits
|
||||
# for readiness lazily. This is the single biggest warm-startup win.
|
||||
_9r_start_task = asyncio.create_task(ensure_9router())
|
||||
p_9r_start_task = asyncio.create_task(ensure_9router())
|
||||
except Exception as e:
|
||||
logger.debug(f"9Router auto-start skipped: {e}")
|
||||
|
||||
@@ -225,13 +225,13 @@ async def service_lifespan():
|
||||
pass
|
||||
p_drain_task = None
|
||||
|
||||
if _9r_start_task and not _9r_start_task.done():
|
||||
_9r_start_task.cancel()
|
||||
if p_9r_start_task and not p_9r_start_task.done():
|
||||
p_9r_start_task.cancel()
|
||||
try:
|
||||
await _9r_start_task
|
||||
await p_9r_start_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
_9r_start_task = None
|
||||
p_9r_start_task = None
|
||||
|
||||
try:
|
||||
from backend.apps.nine_router import stop as stop_9router
|
||||
|
||||
@@ -452,7 +452,7 @@ async def search(body: SearchBody) -> dict:
|
||||
# through to the slower-but-grounded backends.
|
||||
from backend.apps.agents.tools.web import WebSearchTool, DDGRateLimited
|
||||
try:
|
||||
text = await WebSearchTool._search_ddg(body.query, body.num_results)
|
||||
text = await WebSearchTool.search_ddg(body.query, body.num_results)
|
||||
except DDGRateLimited:
|
||||
# Surface the throttle as a recorded error (not a silent None) so the
|
||||
# caller can see WHY we fell through to a slower backend.
|
||||
|
||||
+10
-10
@@ -63,10 +63,10 @@ def get_auth_token() -> str:
|
||||
class p_TokenScrubFilter(logging.Filter):
|
||||
"""Logging filter that redacts the install token from log records (defense in depth)."""
|
||||
|
||||
_PLACEHOLDER = "<REDACTED:openswarm-token>"
|
||||
P_PLACEHOLDER = "<REDACTED:openswarm-token>"
|
||||
|
||||
@staticmethod
|
||||
def _args_might_contain_token(args) -> bool:
|
||||
def p_args_might_contain_token(args) -> bool:
|
||||
"""Cheap pre-check; avoids eager %-formatting on the >99% of records that don't mention the token."""
|
||||
if not args:
|
||||
return False
|
||||
@@ -81,7 +81,7 @@ class p_TokenScrubFilter(logging.Filter):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _scrub_args(cls, args):
|
||||
def p_scrub_args(cls, args):
|
||||
"""Scrub token from args while preserving tuple/dict shape; uvicorn's AccessFormatter unpacks args as a 5-tuple and explodes on None."""
|
||||
if args is None:
|
||||
return args
|
||||
@@ -91,7 +91,7 @@ class p_TokenScrubFilter(logging.Filter):
|
||||
if isinstance(v, str) and TOKEN in v:
|
||||
if new_dict is None:
|
||||
new_dict = dict(args)
|
||||
new_dict[k] = v.replace(TOKEN, cls._PLACEHOLDER)
|
||||
new_dict[k] = v.replace(TOKEN, cls.P_PLACEHOLDER)
|
||||
return new_dict if new_dict is not None else args
|
||||
if isinstance(args, tuple):
|
||||
new_list = None
|
||||
@@ -99,10 +99,10 @@ class p_TokenScrubFilter(logging.Filter):
|
||||
if isinstance(v, str) and TOKEN in v:
|
||||
if new_list is None:
|
||||
new_list = list(args)
|
||||
new_list[i] = v.replace(TOKEN, cls._PLACEHOLDER)
|
||||
new_list[i] = v.replace(TOKEN, cls.P_PLACEHOLDER)
|
||||
return tuple(new_list) if new_list is not None else args
|
||||
if isinstance(args, str) and TOKEN in args:
|
||||
return args.replace(TOKEN, cls._PLACEHOLDER)
|
||||
return args.replace(TOKEN, cls.P_PLACEHOLDER)
|
||||
return args
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool: # pragma: no cover (defensive)
|
||||
@@ -110,19 +110,19 @@ class p_TokenScrubFilter(logging.Filter):
|
||||
return True
|
||||
# Fast path: skip eager %-formatting on records that don't mention the token.
|
||||
raw_msg = record.msg if isinstance(record.msg, str) else ""
|
||||
if TOKEN not in raw_msg and not self._args_might_contain_token(record.args):
|
||||
if TOKEN not in raw_msg and not self.p_args_might_contain_token(record.args):
|
||||
return True
|
||||
# Slow path: in-place args rewrite (preserves shape for AccessFormatter), then re-render to catch tokens buried in custom reprs.
|
||||
try:
|
||||
if isinstance(record.msg, str) and TOKEN in record.msg:
|
||||
record.msg = record.msg.replace(TOKEN, self._PLACEHOLDER)
|
||||
scrubbed = self._scrub_args(record.args)
|
||||
record.msg = record.msg.replace(TOKEN, self.P_PLACEHOLDER)
|
||||
scrubbed = self.p_scrub_args(record.args)
|
||||
if scrubbed is not record.args:
|
||||
record.args = scrubbed
|
||||
try:
|
||||
rendered = record.getMessage()
|
||||
if TOKEN in rendered:
|
||||
record.msg = rendered.replace(TOKEN, self._PLACEHOLDER)
|
||||
record.msg = rendered.replace(TOKEN, self.P_PLACEHOLDER)
|
||||
record.args = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -35,7 +35,7 @@ class MainApp:
|
||||
# per-SubApp markers and a cold-start stall can only be guessed at.
|
||||
# One perf_counter + flushed print per app pins exactly which
|
||||
# lifespan (or the cold first-touch I/O entering it) dominates.
|
||||
_boot_t0 = time.perf_counter()
|
||||
p_boot_t0 = time.perf_counter()
|
||||
for sub_app in sub_apps:
|
||||
debug(sub_app.name)
|
||||
_t0 = time.perf_counter()
|
||||
@@ -43,7 +43,7 @@ class MainApp:
|
||||
_dt = (time.perf_counter() - _t0) * 1000
|
||||
if _dt > 50: # only flag a slow lifespan; keeps boot logs quiet
|
||||
print(f"[perf] lifespan {sub_app.name} t={_dt:.0f}ms", flush=True)
|
||||
print(f"[perf] lifespans-total t={(time.perf_counter() - _boot_t0) * 1000:.0f}ms", flush=True)
|
||||
print(f"[perf] lifespans-total t={(time.perf_counter() - p_boot_t0) * 1000:.0f}ms", flush=True)
|
||||
_port = os.environ.get("OPENSWARM_PORT", "8324")
|
||||
print(f"\nCheck out the API docs at: http://127.0.0.1:{_port}/docs\n")
|
||||
yield
|
||||
|
||||
@@ -7,7 +7,7 @@ import uuid
|
||||
|
||||
from backend.config.paths import DATA_ROOT
|
||||
|
||||
_INSTALL_ID_FILE = os.path.join(DATA_ROOT, "install_id")
|
||||
P_INSTALL_ID_FILE = os.path.join(DATA_ROOT, "install_id")
|
||||
_cached: str | None = None
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ def get_install_id() -> str:
|
||||
return _cached
|
||||
|
||||
try:
|
||||
with open(_INSTALL_ID_FILE, "r", encoding="utf-8") as f:
|
||||
with open(P_INSTALL_ID_FILE, "r", encoding="utf-8") as f:
|
||||
existing = f.read().strip()
|
||||
if _looks_like_uuid(existing):
|
||||
if p_looks_like_uuid(existing):
|
||||
_cached = existing
|
||||
return _cached
|
||||
except FileNotFoundError:
|
||||
@@ -29,8 +29,8 @@ def get_install_id() -> str:
|
||||
pass
|
||||
|
||||
fresh = str(uuid.uuid4())
|
||||
os.makedirs(os.path.dirname(_INSTALL_ID_FILE) or ".", exist_ok=True)
|
||||
fd = os.open(_INSTALL_ID_FILE, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
|
||||
os.makedirs(os.path.dirname(P_INSTALL_ID_FILE) or ".", exist_ok=True)
|
||||
fd = os.open(P_INSTALL_ID_FILE, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
os.write(fd, fresh.encode("utf-8"))
|
||||
finally:
|
||||
@@ -39,7 +39,7 @@ def get_install_id() -> str:
|
||||
return _cached
|
||||
|
||||
|
||||
def _looks_like_uuid(s: str) -> bool:
|
||||
def p_looks_like_uuid(s: str) -> bool:
|
||||
if len(s) != 36:
|
||||
return False
|
||||
try:
|
||||
|
||||
@@ -21,12 +21,12 @@ import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_write_lock = threading.Lock()
|
||||
p_write_lock = threading.Lock()
|
||||
|
||||
|
||||
def atomic_write_json(path: str, payload, *, indent: int = 2) -> None:
|
||||
directory = os.path.dirname(path) or "."
|
||||
with _write_lock:
|
||||
with p_write_lock:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(prefix=".tmp-", suffix=".json", dir=directory)
|
||||
try:
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
P_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
|
||||
p_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
|
||||
|
||||
if _is_packaged:
|
||||
if p_is_packaged:
|
||||
if sys.platform == "darwin":
|
||||
_app_support = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "OpenSwarm")
|
||||
p_app_support = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "OpenSwarm")
|
||||
elif sys.platform == "win32":
|
||||
_app_support = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "OpenSwarm")
|
||||
p_app_support = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "OpenSwarm")
|
||||
else:
|
||||
_app_support = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")), "OpenSwarm")
|
||||
DATA_ROOT = os.path.join(_app_support, "data")
|
||||
p_app_support = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")), "OpenSwarm")
|
||||
DATA_ROOT = os.path.join(p_app_support, "data")
|
||||
else:
|
||||
DATA_ROOT = os.path.join(_BACKEND_DIR, "data")
|
||||
DATA_ROOT = os.path.join(P_BACKEND_DIR, "data")
|
||||
|
||||
SESSIONS_DIR = os.path.join(DATA_ROOT, "sessions")
|
||||
TOOLS_DIR = os.path.join(DATA_ROOT, "tools")
|
||||
@@ -34,4 +34,4 @@ TRUSTED_SENSITIVE_PATHS_PATH = os.path.join(DATA_ROOT, "trusted_sensitive_paths.
|
||||
# Per-install auth token for the localhost API; see auth.py.
|
||||
AUTH_TOKEN_FILE = os.path.join(DATA_ROOT, "auth.token")
|
||||
|
||||
BACKEND_DIR = _BACKEND_DIR
|
||||
BACKEND_DIR = P_BACKEND_DIR
|
||||
|
||||
+6
-6
@@ -79,10 +79,10 @@ install_token_scrubber()
|
||||
try:
|
||||
import uuid as p_uuid
|
||||
from backend.apps.settings.store import load_settings as p_load_boot_settings, save_settings as p_save_boot_settings
|
||||
_boot_settings = p_load_boot_settings()
|
||||
if not getattr(_boot_settings, "installation_id", None):
|
||||
_boot_settings.installation_id = p_uuid.uuid4().hex
|
||||
p_save_boot_settings(_boot_settings)
|
||||
p_boot_settings = p_load_boot_settings()
|
||||
if not getattr(p_boot_settings, "installation_id", None):
|
||||
p_boot_settings.installation_id = p_uuid.uuid4().hex
|
||||
p_save_boot_settings(p_boot_settings)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -956,7 +956,7 @@ if __name__ == "__main__":
|
||||
|
||||
import uvicorn.config
|
||||
|
||||
class _ReadyServer(uvicorn.Server):
|
||||
class p_ReadyServer(uvicorn.Server):
|
||||
"""Subclass that prints a machine-readable READY line on startup."""
|
||||
async def startup(self, sockets=None):
|
||||
await super().startup(sockets)
|
||||
@@ -966,6 +966,6 @@ if __name__ == "__main__":
|
||||
uvicorn.run("backend.main:app", host=args.host, port=args.port, reload=True)
|
||||
else:
|
||||
config = uvicorn.Config("backend.main:app", host=args.host, port=args.port)
|
||||
server = _ReadyServer(config)
|
||||
server = p_ReadyServer(config)
|
||||
import asyncio
|
||||
asyncio.run(server.serve())
|
||||
|
||||
@@ -6,7 +6,7 @@ What this proves:
|
||||
3. AppRuntimeManager.stop_all() resumes SIGSTOP'd idle runtimes before reaping (otherwise the SIGTERM is queued and the process never dies).
|
||||
4. is_port_free() correctly detects collisions.
|
||||
5. write_env_value() updates a single key without clobbering siblings.
|
||||
6. _start_new_mode() rewrites .env's FRONTEND_PORT when the persisted port is in use, and the spawned child sees the rewritten value.
|
||||
6. p_start_new_mode() rewrites .env's FRONTEND_PORT when the persisted port is in use, and the spawned child sees the rewritten value.
|
||||
7. Same collision-rewrite happens for BACKEND_PORT when it's not "NONE".
|
||||
|
||||
Run with: backend/.venv/bin/python backend/tests/test_outputs_runtime_cleanup.py
|
||||
|
||||
@@ -40,13 +40,13 @@ def p_no_network(monkeypatch):
|
||||
def p_ddg_returns(monkeypatch, text):
|
||||
async def p_f(query, num):
|
||||
return text
|
||||
monkeypatch.setattr(WebSearchTool, "_search_ddg", staticmethod(p_f))
|
||||
monkeypatch.setattr(WebSearchTool, "search_ddg", staticmethod(p_f))
|
||||
|
||||
|
||||
def p_ddg_throttled(monkeypatch):
|
||||
async def p_f(query, num):
|
||||
raise DDGRateLimited(query)
|
||||
monkeypatch.setattr(WebSearchTool, "_search_ddg", staticmethod(p_f))
|
||||
monkeypatch.setattr(WebSearchTool, "search_ddg", staticmethod(p_f))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -61,7 +61,7 @@ P_HTML_WITH_AD = """
|
||||
async def test_202_raises_rate_limited_not_empty(monkeypatch):
|
||||
p_patch_client(monkeypatch, p_FakeResp(202, "<html>throttle challenge, no results</html>"))
|
||||
with pytest.raises(DDGRateLimited):
|
||||
await WebSearchTool._search_ddg("anything", 5)
|
||||
await WebSearchTool.search_ddg("anything", 5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -76,7 +76,7 @@ async def test_execute_reports_rate_limit_clearly(monkeypatch):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ads_are_stripped_real_results_kept(monkeypatch):
|
||||
p_patch_client(monkeypatch, p_FakeResp(200, P_HTML_WITH_AD))
|
||||
out = await WebSearchTool._search_ddg("topic", 5)
|
||||
out = await WebSearchTool.search_ddg("topic", 5)
|
||||
assert "example.com/real" in out
|
||||
assert "Real Result Title" in out
|
||||
# the sponsored row and its tracker URL must not appear
|
||||
@@ -89,5 +89,5 @@ async def test_ads_are_stripped_real_results_kept(monkeypatch):
|
||||
async def test_genuinely_empty_is_not_a_rate_limit(monkeypatch):
|
||||
# 200 with no result blocks is a real empty result set, not a throttle.
|
||||
p_patch_client(monkeypatch, p_FakeResp(200, "<html><body>nothing here</body></html>"))
|
||||
out = await WebSearchTool._search_ddg("zxcvqwer no hits", 5)
|
||||
out = await WebSearchTool.search_ddg("zxcvqwer no hits", 5)
|
||||
assert out == ""
|
||||
|
||||
Reference in New Issue
Block a user