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
20 changes: 17 additions & 3 deletions loopx/cli_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,12 @@ def _build_selected_parser(command: str) -> LoopXArgumentParser:
return parser


def dispatch_common_command(
def _dispatch_common_command(
args: argparse.Namespace,
*,
registry_path: Path,
allow_missing_registry: bool,
) -> int | None:
"""Dispatch one selected command through the shared canonical wiring."""

if args.command == "change-window":
from .capabilities.repository_change_window.cli import handle_repository_change_window_command

Expand Down Expand Up @@ -325,6 +323,22 @@ def dispatch_common_command(
return None


def dispatch_common_command(
args: argparse.Namespace,
*,
registry_path: Path,
allow_missing_registry: bool,
) -> int | None:
from .control_plane.effect_runtime import effect_runtime_request_scope

with effect_runtime_request_scope():
return _dispatch_common_command(
args,
registry_path=registry_path,
allow_missing_registry=allow_missing_registry,
)


def _dispatch_selected(args: argparse.Namespace, raw_argv: list[str]) -> int:
args.format = resolve_global_output_format(args)
guard_result = enforce_native_controller_guard(args)
Expand Down
75 changes: 73 additions & 2 deletions loopx/control_plane/effect_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
import tempfile
import time
import uuid
from collections.abc import Mapping
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from threading import Lock
from typing import IO, Any

from ..file_lock import process_is_alive
Expand All @@ -38,6 +42,46 @@
_RuntimeSourceSnapshot = tuple[tuple[str, int, int, int], ...]


@dataclass(frozen=True)
class _RuntimeRevision:
fingerprint: str


class _RequestRuntimeRevision:
def __init__(self) -> None:
self._lock = Lock()
self._revision: _RuntimeRevision | None = None
self._scope_count = 1
self._closed = False

def try_join(self) -> bool:
with self._lock:
if self._closed:
return False
self._scope_count += 1
return True

def resolve(self) -> _RuntimeRevision:
with self._lock:
if not self._closed:
if self._revision is None:
self._revision = _RuntimeRevision(_runtime_fingerprint())
return self._revision
return _RuntimeRevision(_runtime_fingerprint())

def leave(self) -> None:
with self._lock:
self._scope_count -= 1
if self._scope_count == 0:
self._closed = True
self._revision = None


_REQUEST_RUNTIME_REVISION: ContextVar[_RequestRuntimeRevision | None] = (
ContextVar("loopx_effect_runtime_request_revision", default=None)
)


class EffectRuntimeRemoteError(RuntimeError):
"""A typed exception returned by the managed TypeScript runtime."""

Expand Down Expand Up @@ -235,6 +279,33 @@ def _runtime_fingerprint() -> str:
) from exc


@contextmanager
def effect_runtime_request_scope() -> Iterator[None]:
"""Pin one managed runtime revision for a logical request."""

current = _REQUEST_RUNTIME_REVISION.get()
if current is not None and current.try_join():
try:
yield
finally:
current.leave()
return
state = _RequestRuntimeRevision()
token = _REQUEST_RUNTIME_REVISION.set(state)
try:
yield
finally:
state.leave()
_REQUEST_RUNTIME_REVISION.reset(token)


def _runtime_fingerprint_for_request() -> str:
state = _REQUEST_RUNTIME_REVISION.get()
if state is None:
return _runtime_fingerprint()
return state.resolve().fingerprint


def _runtime_dir() -> Path:
owner = str(getattr(os, "getuid", lambda: Path.home())())
suffix = hashlib.sha256(owner.encode("utf-8")).hexdigest()[:12]
Expand Down Expand Up @@ -697,7 +768,7 @@ def effect_runtime_request(
) -> dict[str, Any]:
"""Call the managed TS runtime, retrying only idempotent typed effects."""

fingerprint = _runtime_fingerprint()
fingerprint = _runtime_fingerprint_for_request()
info_path = _runtime_info_path(fingerprint)
request_id = str(uuid.uuid4())
last_error: OSError | RuntimeError | None = None
Expand Down
36 changes: 19 additions & 17 deletions loopx/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

from .control_plane import compact_control_plane_policy
from .control_plane.effect_runtime import effect_runtime_request_scope
from .control_plane.status.collection import (
StatusCollectionContext,
collect_status as _collect_status_read_model,
Expand Down Expand Up @@ -1284,20 +1285,21 @@ def collect_status(
activation_state_filter: str | None = None,
agent_lane_id: str | None = None,
) -> dict[str, Any]:
return _collect_status_read_model(
registry_path=registry_path,
runtime_root_override=runtime_root_override,
scan_roots=scan_roots,
limit=limit,
include_task_graph=include_task_graph,
goal_id=goal_id,
available_capabilities=available_capabilities,
include_public_boundary_scan=include_public_boundary_scan,
recent_run_limit=recent_run_limit,
include_goal_subagent_configuration=(
include_goal_subagent_configuration
),
activation_state_filter=activation_state_filter,
agent_lane_id=agent_lane_id,
context=build_status_collection_context(),
)
with effect_runtime_request_scope():
return _collect_status_read_model(
registry_path=registry_path,
runtime_root_override=runtime_root_override,
scan_roots=scan_roots,
limit=limit,
include_task_graph=include_task_graph,
goal_id=goal_id,
available_capabilities=available_capabilities,
include_public_boundary_scan=include_public_boundary_scan,
recent_run_limit=recent_run_limit,
include_goal_subagent_configuration=(
include_goal_subagent_configuration
),
activation_state_filter=activation_state_filter,
agent_lane_id=agent_lane_id,
context=build_status_collection_context(),
)
Loading
Loading