From 625e82fce953944d780c3d0107937197f83079cc Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:16:54 -0400 Subject: [PATCH 01/13] Admit custom persistence and explicit catalog route before dispatch --- .../custom_registration_admission_20260929.md | 79 +++++++++++ openhcs/agent/capabilities.py | 7 +- openhcs/agent/dto/functions.py | 77 +++++++++- .../endpoint_function_catalog_service.py | 45 +++++- .../services/function_catalog_service.py | 34 ++++- openhcs/core/xdg_paths.py | 13 +- openhcs/mcp/context.py | 8 +- .../processing/custom_functions/manager.py | 40 +++++- openhcs/runtime/zmq_control.py | 20 +++ openhcs/runtime/zmq_execution_client.py | 14 ++ .../test_custom_registration_admission.py | 134 ++++++++++++++++++ 11 files changed, 450 insertions(+), 21 deletions(-) create mode 100644 docs/plans/custom_registration_admission_20260929.md create mode 100644 tests/unit/agent/test_custom_registration_admission.py diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md new file mode 100644 index 0000000000..beb2204f64 --- /dev/null +++ b/docs/plans/custom_registration_admission_20260929.md @@ -0,0 +1,79 @@ +# Custom-function admission (#230) + +Implementation owner: Lovelace; integration/live scheduling owner: coordinator. +Source tree: `/home/ts/wt/openhcs-custom-function-admission-20260929`. +Base: current `openhcsdev/main` 6c2bc4671, integrated normally. + +## Verified boundary and checkpoint + +The preserved H002 receipt033 timed out, but the shared execution log records +saving `/home/ts/.local/share/openhcs/custom_functions/detect_h002_centres.py` at +22:00:08 UTC. The missing cache selector selected shared7777. Documentation +already requires the selector; additional prose does not enforce admission. +No original request is replayed, no saved side effect is removed, and no assay +source, pixels or output is used in the regression fixtures. + +Registration now requires an explicit `ExecutionConnectionSpec` port. Persisted +registration also requires the intended native store and public function name. +`AgentPathPolicy` admits that store and its exact owner-derived source path +before even creating an execution client. A read-only control request asks the +selected server's existing `CustomFunctionManager` owner for its actual store +without source evaluation, catalog preparation or directory creation. A +different or unsupported server fails before executable source is dispatched. +The server repeats admission before evaluation and persistence; caller roots +are internal transport state, not a generated public MCP parameter. + +Successful admission selects the existing catalog connection for subsequent +discovery/reference operations. Post-dispatch transport failure is reported as +`custom_function_registration_uncertain`; it does not retry or select a fallback +endpoint. Existing control timeout limits are unchanged. + +## Focused evidence and resource bounds + +Explicit shared-venv Python with this worktree and all eight recorded submodule +`src` paths: OpenHCS, ObjectState, metaclass_registry and PolyStore import paths +were verified in this source tree. Ten provider-free tests pass in 2.41 seconds, +with 181760 KiB peak RSS; available RAM after the check was 14.26 GiB. The resource +guard warned about historical swap; no heavy runtime was started. The tests +cover missing-route denial, ordinary/ancestor/file-symlink denial before client +creation, native-store mismatch before mutation, exact selected route, +single-dispatch uncertainty, read-only destination lookup and public-authority +field exclusion. `git diff --check` and new-test Ruff checks pass. Whole touched +source Ruff is not claimed clean: existing unrelated findings remain. + +Owned disposable fixtures: +`/home/ts/.cache/agent-scratch/openhcs-registration-230-20260929/pytest`. +No MCP/JVM/GUI handle was started. Tests are source evidence, not installed or +live acceptance, and the frozen installed 17/228 tree remains untouched. + +## Actual pattern review (focused source, not a full NRA proof) + +- BOUND-2/BOUND-6: existing `AgentPathPolicy` owns write permission; + `CustomFunctionManager` and XDG helpers own paths. A different native storage + declaration changes that owner, not a new endpoint-to-filename registry. +- IMPL-12: replaced the manager's repeated name-to-file construction with its + `source_path_for_name`, consumed by admission and actual save. New public names + take the same helper, not a second source parser. +- MEMB-2/MEMB-3: the new control action declares its message on the existing + request/strategy family. Existing registration/discovery registries remain + derived; no companion roster, new runner or duplicate custom registry. +- IMPL-13: admission extends the existing DTO, endpoint service, typed control + protocol and atomic manager persistence, rather than forking registration. +- IDEN-8: endpoint process-token binding remains the immediate next change; + reuse existing `ProcessIdentity` (PID plus creation time), not a bare-PID or + new UUID authority. This checkpoint does not claim race-proof route identity. + +## Remaining acceptance (not closed) + +Bind destination admission to the existing process-identity token and verify +the same owner before server evaluation. Exercise the real generated MCP +signature and native manager/registry preservation. After release of the +technical slot, run controlled owned-vs-shared endpoint, escaping-path and +delayed-response checks, plus register/discover/compile/execute on a tiny +synthetic function through the actual installed MCP path. No scientific +execution, JVM, installation or foreign namespace cleanup is authorized here. + +Ordinary persistence admission is not a sandbox for arbitrary authorized Python; +filesystem races and hostile remote servers are not claimed globally solved. +Broad #206/#16 preparation/ROI scope and its 26 combined-suite failures remain +in their separate trees/PRs. No ROI profile or preservation pass is invented. diff --git a/openhcs/agent/capabilities.py b/openhcs/agent/capabilities.py index 03dd746dce..12fc5b8d9b 100644 --- a/openhcs/agent/capabilities.py +++ b/openhcs/agent/capabilities.py @@ -1895,7 +1895,10 @@ class RegisterCustomFunctionCapability(FunctionCatalogCapability): description = ( "Validates, registers, and optionally persists custom function Python " "source through CustomFunctionManager, then returns registry function_id " - "values for MCP pipeline authoring." + "values for MCP pipeline authoring. Requires an explicit execution port; " + "persist=true also requires the endpoint's exact storage_dir and function_name " + "under AgentPathPolicy writable roots before dispatch. A transport timeout " + "is uncertain, not proof that no source or registry mutation occurred." ) service = "function_catalog" exposition = FunctionCatalogCapability.exposition.refine( @@ -1905,7 +1908,7 @@ class RegisterCustomFunctionCapability(FunctionCatalogCapability): side_effects = ("writes_custom_function_file", "updates_function_registry") input_contract = CustomFunctionRegistrationRequest output_contract = CustomFunctionRegistrationResult - request_invocation = AgentDataclassRequestServiceInvocation( + request_invocation = AgentFromFieldsServiceInvocation( service=lambda context: context.function_catalog, method=lambda service, request: service.register_custom_function(request), ) diff --git a/openhcs/agent/dto/functions.py b/openhcs/agent/dto/functions.py index f75859463a..fecdb07288 100644 --- a/openhcs/agent/dto/functions.py +++ b/openhcs/agent/dto/functions.py @@ -6,14 +6,18 @@ import json from abc import ABC from collections.abc import Mapping -from dataclasses import dataclass +from dataclasses import dataclass, field, replace from enum import Enum +from pathlib import Path from typing import Any, ClassVar, Generic, Self, TypeVar, cast +from zmqruntime.config import TransportMode from zmqruntime.messages import MessageFields, ResponseType from zmqruntime.startup import EndpointStartupStatus from openhcs.agent.dto.common import SCHEMA_VERSION, AgentResultEnvelope +from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec +from openhcs.agent.path_policy import AgentPathPolicy from openhcs.core.artifacts import ArtifactViewerStreaming from openhcs.core.function_reference import FunctionReference @@ -28,6 +32,7 @@ class FunctionCatalogControlMessageType(str, Enum): READ_DETAIL = "openhcs_function_detail_read" READ_REFERENCE = "openhcs_function_reference_read" REGISTER_CUSTOM = "openhcs_custom_function_register" + CUSTOM_REGISTRATION_DESTINATION = "openhcs_custom_function_registration_destination" class FunctionCatalogControlField(str, Enum): @@ -114,9 +119,67 @@ class CustomFunctionRegistrationRequest(FunctionCatalogControlRequestABC): source_code: str persist: bool = True compact_signature: bool = True + connection: ExecutionConnectionSpec = field(default_factory=ExecutionConnectionSpec) + function_name: str | None = None + storage_dir: str | None = None + admission_policy: AgentPathPolicy | None = field(default=None, repr=False, compare=False) message_type = FunctionCatalogControlMessageType.REGISTER_CUSTOM + @classmethod + def from_fields( + cls, *, source_code: str, persist: bool = True, + compact_signature: bool = True, function_name: str | None = None, + storage_dir: str | None = None, host: str = "localhost", + port: int | None = None, transport_mode: TransportMode | None = None, + persistent: bool = True, + ) -> Self: + """Public boundary: callers cannot supply their own write authority.""" + return cls( + source_code=source_code, persist=persist, compact_signature=compact_signature, + function_name=function_name, storage_dir=storage_dir, + connection=ExecutionConnectionSpec( + host=host, port=port, transport_mode=transport_mode, persistent=persistent, + ), + ) + + def admitted(self, policy: AgentPathPolicy) -> Self: + """Admit the exact destination before endpoint dispatch or source evaluation.""" + from openhcs.processing.custom_functions.manager import CustomFunctionManager + + self.connection.require_port("Custom function registration") + if self.persist: + if self.function_name is None or self.storage_dir is None: + raise ValueError("Persisted registration requires function_name and the endpoint's storage_dir.") + root = Path(self.storage_dir).expanduser() + if not root.is_absolute(): + raise ValueError("Custom function storage_dir must be absolute.") + policy.assert_writable(root) + policy.assert_writable(CustomFunctionManager.source_path_for_name(root, self.function_name)) + return replace(self, admission_policy=policy) + + +@dataclass(frozen=True, slots=True) +class CustomFunctionRegistrationDestinationRequest(FunctionCatalogControlRequestABC): + """Read the persistence owner's exact path without evaluating source.""" + + function_name: str | None = None + message_type = FunctionCatalogControlMessageType.CUSTOM_REGISTRATION_DESTINATION + + +@dataclass(frozen=True, slots=True) +class CustomFunctionRegistrationDestination: + storage_dir: str + source_file_path: str | None + + def require_request(self, request: CustomFunctionRegistrationRequest) -> None: + """Reject a different native store before sending executable source.""" + if request.persist and Path(self.storage_dir).resolve(strict=False) != Path(request.storage_dir).resolve(strict=False): + raise ValueError( + f"Selected endpoint owns custom storage {self.storage_dir}, " + f"not requested {request.storage_dir}; no source was dispatched." + ) + class FunctionParameterSource(str, Enum): """Nominal source of one callable parameter in agent-facing docs.""" @@ -463,6 +526,7 @@ class CustomFunctionRegistrationResult(AgentResultEnvelope): source_file_paths: tuple[str, ...] = () functions: tuple[FunctionCatalogEntry, ...] = () next_steps: tuple[str, ...] = () + connection: ExecutionConnectionSpec = field(default_factory=ExecutionConnectionSpec) class CustomFunctionRegistrationControlResponse( @@ -478,6 +542,17 @@ def result(self) -> CustomFunctionRegistrationResult: return self.value +class CustomFunctionRegistrationDestinationControlResponse( + FunctionCatalogControlResponseBase[CustomFunctionRegistrationDestination] +): + field = FunctionCatalogControlField.RESULT + value_type = CustomFunctionRegistrationDestination + + @property + def destination(self) -> CustomFunctionRegistrationDestination: + return self.value + + def catalog_page( *, items: tuple[FunctionCatalogEntry, ...], diff --git a/openhcs/agent/services/endpoint_function_catalog_service.py b/openhcs/agent/services/endpoint_function_catalog_service.py index d8f8a020e5..09ba916955 100644 --- a/openhcs/agent/services/endpoint_function_catalog_service.py +++ b/openhcs/agent/services/endpoint_function_catalog_service.py @@ -6,7 +6,7 @@ import threading from collections.abc import Callable, Mapping from concurrent.futures import CancelledError, Future -from dataclasses import dataclass +from dataclasses import dataclass, replace from types import MappingProxyType from typing import TYPE_CHECKING @@ -14,6 +14,7 @@ from openhcs.agent.dto.functions import ( DEFAULT_FUNCTION_DETAIL_DOC_CHARS, + CustomFunctionRegistrationDestinationRequest, CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogControlRequest, @@ -24,6 +25,8 @@ FunctionReferenceControlRequest, FunctionSearchRequest, ) +from openhcs.agent.exceptions import AgentFacingErrorMixin +from openhcs.agent.path_policy import AgentPathPolicy from openhcs.agent.services.function_catalog_service import FunctionCatalogServiceABC from openhcs.runtime.zmq_config import OpenHCSZMQConfig @@ -34,6 +37,18 @@ logger = logging.getLogger(__name__) +class CustomFunctionRegistrationUncertainError(AgentFacingErrorMixin, RuntimeError): + agent_error_code = "custom_function_registration_uncertain" + agent_error_hint = "Preserve this request and endpoint receipt. Do not replay or fall back; persistence may have completed." + + def __init__(self, request: CustomFunctionRegistrationRequest): + super().__init__( + f"Registration observation failed after invoking {request.connection.transport_endpoint()}; " + f"destination={request.storage_dir}, function={request.function_name!r}. " + "Outcome is uncertain; source or registry mutation may have completed." + ) + + FunctionCatalogClientFactory = Callable[ [OpenHCSZMQConfig], "ZMQExecutionClient", @@ -166,8 +181,10 @@ def __init__( config_provider: Callable[[], OpenHCSZMQConfig], *, client_factory: FunctionCatalogClientFactory | None = None, + path_policy: AgentPathPolicy | None = None, ) -> None: self._config_provider = config_provider + self._path_policy = path_policy or AgentPathPolicy.from_environment() self._client_factory = client_factory or self._new_client self._client_session: FunctionCatalogClientSession | None = None self._endpoint_state: FunctionCatalogEndpointState | None = None @@ -350,14 +367,34 @@ def register_custom_function( ) -> CustomFunctionRegistrationResult: """Register source at the endpoint and project ephemeral source locally.""" - endpoint = self._config_provider() - result = self._client_for(endpoint).register_custom_function(request) + request = request.admitted(self._path_policy) + endpoint = replace( + self._config_provider(), + default_port=request.connection.require_port("Custom function registration"), + client_host=request.connection.host, + transport_mode=request.connection.transport_endpoint().transport_mode, + persistent=request.connection.persistent, + ) + client = self._client_for(endpoint) + destination = client.custom_function_registration_destination( + CustomFunctionRegistrationDestinationRequest(function_name=request.function_name) + ) + destination.require_request(request) + if request.persist: + self._path_policy.assert_writable(destination.storage_dir) + self._path_policy.assert_writable(destination.source_file_path) + self.invalidate() + self._config_provider = lambda: endpoint + try: + result = client.register_custom_function(request) + except (TimeoutError, ConnectionError, OSError) as error: + raise CustomFunctionRegistrationUncertainError(request) from error if not request.persist: from openhcs.processing.custom_functions.manager import ( CustomFunctionManager, ) - CustomFunctionManager().register_from_code( + CustomFunctionManager(create_storage=False).register_from_code( request.source_code, persist=False, clear_caches=False, diff --git a/openhcs/agent/services/function_catalog_service.py b/openhcs/agent/services/function_catalog_service.py index f167c93318..e8a8e55e66 100644 --- a/openhcs/agent/services/function_catalog_service.py +++ b/openhcs/agent/services/function_catalog_service.py @@ -8,6 +8,7 @@ from collections.abc import Callable from dataclasses import dataclass from enum import Enum +from pathlib import Path from typing import TYPE_CHECKING from pyqt_reactive.services.parameter_help_service import docstring_info_for_target @@ -25,6 +26,8 @@ DEFAULT_FUNCTION_DETAIL_DOC_CHARS, CellProfilerArtifactBindingSummary, CellProfilerModuleDeclarationSummary, + CustomFunctionRegistrationDestination, + CustomFunctionRegistrationDestinationRequest, CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionArtifactSpec, @@ -37,6 +40,7 @@ catalog_page, ) from openhcs.agent.exceptions import AgentFacingErrorMixin +from openhcs.agent.path_policy import AgentPathPolicy from openhcs.core.artifacts import ( ArtifactInputPlan, ArtifactSpec, @@ -531,7 +535,8 @@ def reference(self, function_id: str) -> "FunctionReference": class FunctionCatalogService(FunctionCatalogServiceABC): """Expose registered OpenHCS processing callables through stable IDs.""" - def __init__(self) -> None: + def __init__(self, path_policy: AgentPathPolicy | None = None) -> None: + self._path_policy = path_policy or AgentPathPolicy.from_environment() self._projection_metadata: dict[str, FunctionMetadata] | None = None self._projections: dict[ tuple[SignatureView, SummaryView], @@ -541,9 +546,17 @@ def __init__(self) -> None: def register_custom_function( self, request: CustomFunctionRegistrationRequest ) -> CustomFunctionRegistrationResult: - manager = custom_function_manager.CustomFunctionManager() + request = request.admitted(request.admission_policy or self._path_policy) + manager = custom_function_manager.CustomFunctionManager(create_storage=False) + if request.persist and manager.storage_dir.resolve(strict=False) != Path(request.storage_dir).resolve(strict=False): + raise ValueError( + f"Selected endpoint owns custom storage {manager.storage_dir}, " + f"not requested {request.storage_dir}; no source was evaluated." + ) registered_functions = manager.register_from_code( - request.source_code, persist=request.persist + request.source_code, persist=request.persist, + expected_function_name=request.function_name, + write_admission=request.admission_policy.assert_writable, ) function_ids = self.function_ids_for_callables(tuple(registered_functions)) entries = tuple( @@ -579,6 +592,21 @@ def register_custom_function( for function_id in function_ids ) ), + connection=request.connection, + ) + + def custom_function_registration_destination( + self, request: CustomFunctionRegistrationDestinationRequest + ) -> CustomFunctionRegistrationDestination: + """Project the native store without construction, source execution or IO.""" + manager_type = custom_function_manager.CustomFunctionManager + storage_dir = manager_type.default_storage_directory() + return CustomFunctionRegistrationDestination( + storage_dir=str(storage_dir), + source_file_path=( + str(manager_type.source_path_for_name(storage_dir, request.function_name)) + if request.function_name is not None else None + ), ) def search( diff --git a/openhcs/core/xdg_paths.py b/openhcs/core/xdg_paths.py index 44ae2a5b68..c0618ec0e5 100644 --- a/openhcs/core/xdg_paths.py +++ b/openhcs/core/xdg_paths.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -def get_openhcs_data_dir() -> Path: +def get_openhcs_data_dir(*, create: bool = True) -> Path: """ Get the OpenHCS data directory following XDG Base Directory Specification. @@ -27,7 +27,8 @@ def get_openhcs_data_dir() -> Path: """ data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) data_dir = data_home / "openhcs" - data_dir.mkdir(parents=True, exist_ok=True) + if create: + data_dir.mkdir(parents=True, exist_ok=True) return data_dir @@ -263,7 +264,9 @@ def get_config_file_path(filename: str, legacy_filename: Optional[str] = None) - return new_path -def get_data_file_path(filename: str, legacy_filename: Optional[str] = None) -> Path: +def get_data_file_path( + filename: str, legacy_filename: Optional[str] = None, *, create: bool = True +) -> Path: """ Get path for a data file, with automatic migration from legacy location. @@ -274,7 +277,9 @@ def get_data_file_path(filename: str, legacy_filename: Optional[str] = None) -> Returns: Path to the data file in XDG location """ - new_path = get_openhcs_data_dir() / filename + new_path = get_openhcs_data_dir(create=create) / filename + if not create: + return new_path # Check if we need to migrate from legacy location if not new_path.exists(): diff --git a/openhcs/mcp/context.py b/openhcs/mcp/context.py index 30edcc0f6e..165795e1cd 100644 --- a/openhcs/mcp/context.py +++ b/openhcs/mcp/context.py @@ -116,7 +116,7 @@ def function_catalog(self) -> "FunctionCatalogServiceABC": FunctionCatalogService, ) - self._function_catalog = FunctionCatalogService() + self._function_catalog = FunctionCatalogService(path_policy=self.path_policy) return self._function_catalog @property @@ -286,8 +286,12 @@ def create_agent_context() -> OpenHCSAgentContext: from openhcs.pyqt_gui.config import load_cached_ui_execution_endpoint_sync endpoint_config = load_cached_ui_execution_endpoint_sync() + path_policy = AgentPathPolicy.from_environment() return OpenHCSAgentContext( - function_catalog=ZMQFunctionCatalogService(lambda: endpoint_config) + path_policy=path_policy, + function_catalog=ZMQFunctionCatalogService( + lambda: endpoint_config, path_policy=path_policy, + ), ) diff --git a/openhcs/processing/custom_functions/manager.py b/openhcs/processing/custom_functions/manager.py index 2587b1da7d..a668905c58 100644 --- a/openhcs/processing/custom_functions/manager.py +++ b/openhcs/processing/custom_functions/manager.py @@ -93,10 +93,24 @@ def require_source(self, expected: CustomFunctionSource) -> None: "recompile the pipeline." ) - def __init__(self): + storage_subdirectory = "custom_functions" + + @classmethod + def default_storage_directory(cls) -> Path: + """Resolve the native store without creating or migrating any files.""" + return get_data_file_path(cls.storage_subdirectory, create=False) + + @staticmethod + def source_path_for_name(storage_dir: Path, function_name: str) -> Path: + """Own source naming for both admission and actual persistence.""" + if not function_name.isidentifier() or function_name.startswith("_"): + raise ValidationError("Custom function name must be a public Python identifier.") + return storage_dir / f"{function_name}.py" + + def __init__(self, *, create_storage: bool = True): """Initialize manager and create storage directory if needed.""" - self.storage_dir: Path = get_data_file_path("custom_functions") - if not self.storage_dir.exists(): + self.storage_dir: Path = self.default_storage_directory() + if create_storage and not self.storage_dir.exists(): self.storage_dir.mkdir(parents=True, exist_ok=True) logger.info(f"Created custom functions directory: {self.storage_dir}") @@ -107,6 +121,8 @@ def register_from_code( *, clear_caches: bool = True, emit_signal: bool = True, + expected_function_name: str | None = None, + write_admission: Callable[[Path], Path] | None = None, ) -> list[Callable]: """ Validate and register the single declaration owned by this source. @@ -127,12 +143,26 @@ def register_from_code( ValueError: If no valid functions found RuntimeError: If function registration fails """ + if persist and write_admission is not None: + if expected_function_name is None: + raise ValueError("Write admission requires an explicit custom function name.") + write_admission(self.storage_dir) + write_admission(self.source_path_for_name(self.storage_dir, expected_function_name)) metadata = self._prepare_source(code) + if expected_function_name is not None and metadata.original_name != expected_function_name: + raise ValidationError( + f"Prepared declaration {metadata.original_name!r} does not match " + f"admitted function {expected_function_name!r}." + ) lifetime = CustomFunctionLifetime.from_persist(persist) with CustomFunctionRuntimeRegistry.lifecycle(): CustomFunctionRuntimeRegistry.ensure_can_publish(metadata) if persist: source_path = self.source_path_for_function(metadata.func) + if write_admission is not None: + write_admission(self.storage_dir) + write_admission(source_path) + self.storage_dir.mkdir(parents=True, exist_ok=True) if source_path.exists(): raise ValueError( f"Custom function '{metadata.original_name}' already exists; " @@ -160,7 +190,7 @@ def register_from_code( def source_path_for_function(self, func: Callable) -> Path: """Return the persisted source path used for a registered function.""" - return self.storage_dir / f"{func.__name__}.py" + return self.source_path_for_name(self.storage_dir, func.__name__) def source_revision(self) -> CustomFunctionSourceRevision: """Return a content-derived revision of all persisted declarations.""" @@ -541,7 +571,7 @@ def _save_function_code(self, func_name: str, code: str) -> None: func_name: Name of function (used as filename) code: Python source code """ - file_path = self.storage_dir / f"{func_name}.py" + file_path = self.source_path_for_name(self.storage_dir, func_name) temp_path = self._write_temporary_source(code) try: os.replace(temp_path, file_path) diff --git a/openhcs/runtime/zmq_control.py b/openhcs/runtime/zmq_control.py index 3f8be7c7b0..4c507ad703 100644 --- a/openhcs/runtime/zmq_control.py +++ b/openhcs/runtime/zmq_control.py @@ -307,6 +307,26 @@ def handle_ready( ).to_control_response() +class CustomFunctionRegistrationDestinationMessageStrategy(ZMQControlMessageStrategy): + """Native store admission must not initialize/evaluate the function catalog.""" + + from openhcs.agent.dto.functions import CustomFunctionRegistrationDestinationRequest + + request_type = CustomFunctionRegistrationDestinationRequest + registry_key = request_type.message_type.value + + def handle(self, message: dict, context: ZMQControlRequestContext) -> dict: + from openhcs.agent.dto.functions import CustomFunctionRegistrationDestinationControlResponse + + try: + request = self.request_type.from_control_payload(message) + return CustomFunctionRegistrationDestinationControlResponse( + value=context.require_function_catalog().custom_function_registration_destination(request), + ).to_control_response() + except Exception as error: + return self.error_response(error) + + class CustomFunctionRegistrationMessageStrategy(FunctionCatalogMessageStrategy): """Register custom source through the execution endpoint catalog owner.""" diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 3f671f197e..2c28adee51 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -911,6 +911,20 @@ def register_custom_function( response ).result + def custom_function_registration_destination(self, request): + """Require the selected endpoint's native admission contract before mutation.""" + from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationDestinationControlResponse, + FunctionCatalogControlPayload, + ) + + if not self.is_connected() and not self.connect(): + raise RuntimeError("Failed to connect to execution server") + response = self._send_control_request( + FunctionCatalogControlPayload.from_request(request).to_dict() + ) + return CustomFunctionRegistrationDestinationControlResponse.from_control_response(response).destination + def _send_function_catalog_control_request( self, request: dict, diff --git a/tests/unit/agent/test_custom_registration_admission.py b/tests/unit/agent/test_custom_registration_admission.py new file mode 100644 index 0000000000..b2bf76867a --- /dev/null +++ b/tests/unit/agent/test_custom_registration_admission.py @@ -0,0 +1,134 @@ +"""Built-in destination and route admission before custom source side effects.""" + +from dataclasses import replace +from pathlib import Path + +import pytest +from zmqruntime.config import TransportMode + +from openhcs.agent.dto.common import SCHEMA_VERSION +from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationDestination, + CustomFunctionRegistrationDestinationRequest, + CustomFunctionRegistrationRequest, + CustomFunctionRegistrationResult, +) +from openhcs.agent.path_policy import AgentPathPolicy, AgentPathPolicyError +from openhcs.agent.services.endpoint_function_catalog_service import ( + CustomFunctionRegistrationUncertainError, + ZMQFunctionCatalogService, +) +from openhcs.agent.services.function_catalog_service import FunctionCatalogService +from openhcs.processing.custom_functions.manager import CustomFunctionManager +from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG + + +def request(root, **changes): + return replace(CustomFunctionRegistrationRequest.from_fields( + source_code="@numpy\ndef boundary_probe(image):\n return image\n", + function_name="boundary_probe", storage_dir=str(root), port=15993, + transport_mode=TransportMode.TCP, + ), **changes) + + +def policy(root): + return AgentPathPolicy.with_roots(readable_roots=(root,), writable_roots=(root,)) + + +def test_missing_explicit_route_rejects_before_client_creation(tmp_path): + made = [] + catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: made.append(endpoint), path_policy=policy(tmp_path)) + with pytest.raises(ValueError, match="explicit port"): + catalog.register_custom_function(CustomFunctionRegistrationRequest(source_code="not evaluated")) + assert not made + + +@pytest.mark.parametrize("escape", ("ordinary", "ancestor", "destination")) +def test_write_escape_rejects_before_endpoint_dispatch(tmp_path, escape): + owned = tmp_path / "owned" + outside = tmp_path / "outside" + owned.mkdir() + outside.mkdir() + sentinel = outside / "boundary_probe.py" + sentinel.write_text("preserve") + root = outside if escape == "ordinary" else owned / "custom" + if escape == "ancestor": + root.symlink_to(outside, target_is_directory=True) + elif escape == "destination": + root.mkdir() + (root / sentinel.name).symlink_to(sentinel) + made = [] + catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: made.append(endpoint), path_policy=policy(owned)) + with pytest.raises(AgentPathPolicyError): + catalog.register_custom_function(request(root)) + assert not made + assert sentinel.read_text() == "preserve" + + +@pytest.mark.parametrize("wrong_store, timeout", ((False, False), (True, False), (False, True))) +def test_exact_owned_route_and_store_precede_mutation(tmp_path, wrong_store, timeout): + root = tmp_path / "custom" + mutations = [] + endpoints = [] + class Client: + def custom_function_registration_destination(self, probe): + assert probe.function_name == "boundary_probe" + native = tmp_path / "foreign" if wrong_store else root + return CustomFunctionRegistrationDestination(str(native), str(native / "boundary_probe.py")) + def register_custom_function(self, admitted): + mutations.append(admitted) + assert admitted.admission_policy == policy(tmp_path) + if timeout: + raise TimeoutError("controlled post-dispatch observation") + return CustomFunctionRegistrationResult(schema_version=SCHEMA_VERSION, connection=admitted.connection) + def disconnect(self): + pass + def factory(endpoint): + endpoints.append(endpoint) + return Client() + catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, + client_factory=factory, path_policy=policy(tmp_path)) + if wrong_store: + with pytest.raises(ValueError, match="no source was dispatched"): + catalog.register_custom_function(request(root)) + assert not mutations + elif timeout: + with pytest.raises(CustomFunctionRegistrationUncertainError, match="uncertain"): + catalog.register_custom_function(request(root)) + assert len(mutations) == 1 + assert catalog._config_provider().default_port == 15993 + else: + result = catalog.register_custom_function(request(root)) + assert result.connection.port == 15993 + assert len(mutations) == 1 + assert catalog._config_provider().default_port == 15993 + assert [endpoint.default_port for endpoint in endpoints] == [15993] + assert not root.exists() + catalog.close() + + +def test_native_destination_query_does_not_create_storage(tmp_path, monkeypatch): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "absent")) + result = FunctionCatalogService(path_policy=policy(tmp_path)).custom_function_registration_destination( + CustomFunctionRegistrationDestinationRequest(function_name="boundary_probe")) + assert Path(result.storage_dir) == CustomFunctionManager.default_storage_directory() + assert Path(result.source_file_path) == CustomFunctionManager.source_path_for_name(Path(result.storage_dir), "boundary_probe") + assert not (tmp_path / "absent").exists() + + +def test_local_denial_precedes_manager_evaluation_and_creation(tmp_path, monkeypatch): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "foreign")) + evaluated = [] + monkeypatch.setattr(CustomFunctionManager, "_prepare_source", lambda *args: evaluated.append(args)) + root = CustomFunctionManager.default_storage_directory() + with pytest.raises(AgentPathPolicyError): + FunctionCatalogService(path_policy=policy(tmp_path / "owned")).register_custom_function(request(root)) + assert not evaluated + assert not (tmp_path / "foreign").exists() + + +def test_public_factory_cannot_expand_write_authority(tmp_path): + with pytest.raises(TypeError, match="admission_policy"): + CustomFunctionRegistrationRequest.from_fields(source_code="", admission_policy=policy(tmp_path)) From 4ffc2b202f960a3dc5ef7d640842cba5f94a8df9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:30:25 -0400 Subject: [PATCH 02/13] Bind custom admission to native server identity and public CLI route --- .../custom_registration_admission_20260929.md | 58 +++- openhcs/agent/dto/functions.py | 51 ++- .../endpoint_function_catalog_service.py | 29 +- .../services/function_catalog_service.py | 10 +- .../dev_client_commands/knowledge_pipeline.py | 28 +- openhcs/runtime/zmq_execution_client.py | 6 +- .../test_custom_registration_admission.py | 322 ++++++++++++++++-- tests/unit/agent/test_mcp_server.py | 8 + tests/unit/test_custom_function_lifecycle.py | 2 +- tests/unit/test_function_catalog_zmq.py | 23 ++ tests/unit/test_function_step_transport.py | 2 +- 11 files changed, 483 insertions(+), 56 deletions(-) diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index beb2204f64..028e0b1899 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -20,8 +20,13 @@ before even creating an execution client. A read-only control request asks the selected server's existing `CustomFunctionManager` owner for its actual store without source evaluation, catalog preparation or directory creation. A different or unsupported server fails before executable source is dispatched. -The server repeats admission before evaluation and persistence; caller roots -are internal transport state, not a generated public MCP parameter. +The destination carries existing `ProcessIdentity` (PID plus creation time). +That exact token is required by the native service before source evaluation and +returned with the typed connection in the mutation receipt. A changed owner or +malformed mutation receipt is not silently accepted. The server repeats both +its own and the caller's admission before evaluation and persistence; caller +roots and the selected token are internal transport state, not public MCP +parameters. The real generated tool signature verifies their exclusion. Successful admission selects the existing catalog connection for subsequent discovery/reference operations. Post-dispatch transport failure is reported as @@ -41,6 +46,32 @@ single-dispatch uncertainty, read-only destination lookup and public-authority field exclusion. `git diff --check` and new-test Ruff checks pass. Whole touched source Ruff is not claimed clean: existing unrelated findings remain. +Follow-up evidence: **94 tests passed, two plugin-disabled configuration warnings, +10.17 seconds, peak RSS 322984 KiB**. This runs the new admission file, existing +custom-function lifecycle and function-catalog ZMQ suites, and three affected +MCP/CLI regressions. It adds native ordinary/ancestor/file-symlink denial with +unchanged registry membership and sentinel bytes, PID-incarnation rejection, +independent native-server policy, exact destination/receipt validation, and +actual manager persistence plus lazy package reopen/invocation on a synthetic +2x2 array. Read-only destination routing is tested with unfinished catalog +preparation and asserts it is never started. The current CLI adapter derives +connection options/defaults from `ExecutionConnectionSpec` and the public +request factory. Its real `python -m openhcs.mcp.dev_client +register-custom-function --help` entrypoint succeeds and exposes those fields; +missing port is rejected before launching a client. + +One attempted extension of that shard could not collect +`test_function_step_transport.py::test_persisted_custom_function_is_importable_from_package`: +the module imports the unbuilt CellProfiler `_granularity_reconstruct` extension. +No tests ran in that failed invocation (exit4, 7.16 seconds, peak373668 KiB). +Its mocked XDG helper is updated to accept the new non-creating projection; +that module is **not claimed passed**. No extension build/install or collection +workaround is performed during the freeze. Subsequent isolated shard result is +the 94-test evidence above, not a claim that this missing dependency disappeared. + +Available RAM stayed above 8 GiB (latest13.4 GiB); guard still warns about +historical11.1 GiB swap. No heavy runtime or validation-lock holder was created. + Owned disposable fixtures: `/home/ts/.cache/agent-scratch/openhcs-registration-230-20260929/pytest`. No MCP/JVM/GUI handle was started. Tests are source evidence, not installed or @@ -59,19 +90,24 @@ live acceptance, and the frozen installed 17/228 tree remains untouched. derived; no companion roster, new runner or duplicate custom registry. - IMPL-13: admission extends the existing DTO, endpoint service, typed control protocol and atomic manager persistence, rather than forking registration. -- IDEN-8: endpoint process-token binding remains the immediate next change; - reuse existing `ProcessIdentity` (PID plus creation time), not a bare-PID or - new UUID authority. This checkpoint does not claim race-proof route identity. +- IDEN-8: destination admission and the mutation receipt reuse existing + `ProcessIdentity` (PID plus creation time), not bare PID/new UUID state. The + new-case witness changes creation time while retaining PID: native registration + rejects before manager evaluation. This is not authentication of hostile + servers or a proof that a remote filesystem is shared with the MCP process. +- BOUND-1/IMPL-13: the existing CLI's public factory/typed connection projects + the new route fields; no internal admission authority is generated as an + option and no alternate registration implementation is added. ## Remaining acceptance (not closed) -Bind destination admission to the existing process-identity token and verify -the same owner before server evaluation. Exercise the real generated MCP -signature and native manager/registry preservation. After release of the -technical slot, run controlled owned-vs-shared endpoint, escaping-path and +After verified closure of the frozen author's owned MCP/viewer and release of +the technical slot, run controlled owned-vs-shared endpoint, escaping-path and delayed-response checks, plus register/discover/compile/execute on a tiny -synthetic function through the actual installed MCP path. No scientific -execution, JVM, installation or foreign namespace cleanup is authorized here. +synthetic function through the actual installed MCP path. The integration owner +controls review, installation and slot handoff; no scientific execution, JVM, +installation or foreign namespace cleanup is authorized to this source worker. +This PR remains draft and references #230 without claiming closure. Ordinary persistence admission is not a sandbox for arbitrary authorized Python; filesystem races and hostile remote servers are not claimed globally solved. diff --git a/openhcs/agent/dto/functions.py b/openhcs/agent/dto/functions.py index fecdb07288..df65397b99 100644 --- a/openhcs/agent/dto/functions.py +++ b/openhcs/agent/dto/functions.py @@ -12,7 +12,7 @@ from typing import Any, ClassVar, Generic, Self, TypeVar, cast from zmqruntime.config import TransportMode -from zmqruntime.messages import MessageFields, ResponseType +from zmqruntime.messages import MessageFields, ProcessIdentity, ResponseType from zmqruntime.startup import EndpointStartupStatus from openhcs.agent.dto.common import SCHEMA_VERSION, AgentResultEnvelope @@ -123,6 +123,7 @@ class CustomFunctionRegistrationRequest(FunctionCatalogControlRequestABC): function_name: str | None = None storage_dir: str | None = None admission_policy: AgentPathPolicy | None = field(default=None, repr=False, compare=False) + server_identity: ProcessIdentity | None = None message_type = FunctionCatalogControlMessageType.REGISTER_CUSTOM @@ -130,9 +131,9 @@ class CustomFunctionRegistrationRequest(FunctionCatalogControlRequestABC): def from_fields( cls, *, source_code: str, persist: bool = True, compact_signature: bool = True, function_name: str | None = None, - storage_dir: str | None = None, host: str = "localhost", + storage_dir: str | None = None, host: str = ExecutionConnectionSpec().host, port: int | None = None, transport_mode: TransportMode | None = None, - persistent: bool = True, + persistent: bool = ExecutionConnectionSpec().persistent, ) -> Self: """Public boundary: callers cannot supply their own write authority.""" return cls( @@ -158,6 +159,16 @@ def admitted(self, policy: AgentPathPolicy) -> Self: policy.assert_writable(CustomFunctionManager.source_path_for_name(root, self.function_name)) return replace(self, admission_policy=policy) + def require_server_identity(self) -> ProcessIdentity: + """Require the same native owner that supplied the admission destination.""" + actual = ProcessIdentity.current() + if self.server_identity != actual: + raise ValueError( + f"Custom registration selected server {self.server_identity!r}, " + f"but reached {actual!r}; no source was evaluated." + ) + return actual + @dataclass(frozen=True, slots=True) class CustomFunctionRegistrationDestinationRequest(FunctionCatalogControlRequestABC): @@ -171,15 +182,44 @@ class CustomFunctionRegistrationDestinationRequest(FunctionCatalogControlRequest class CustomFunctionRegistrationDestination: storage_dir: str source_file_path: str | None + server_identity: ProcessIdentity = field(default_factory=ProcessIdentity.current) def require_request(self, request: CustomFunctionRegistrationRequest) -> None: """Reject a different native store before sending executable source.""" - if request.persist and Path(self.storage_dir).resolve(strict=False) != Path(request.storage_dir).resolve(strict=False): + if not request.persist: + return + from openhcs.processing.custom_functions.manager import CustomFunctionManager + + expected_root = Path(request.storage_dir).expanduser() + expected_file = CustomFunctionManager.source_path_for_name( + expected_root, request.function_name, + ) + if ( + Path(self.storage_dir).resolve(strict=False) != expected_root.resolve(strict=False) + or self.source_file_path is None + or Path(self.source_file_path).resolve(strict=False) != expected_file.resolve(strict=False) + ): raise ValueError( f"Selected endpoint owns custom storage {self.storage_dir}, " - f"not requested {request.storage_dir}; no source was dispatched." + f"source {self.source_file_path}, not requested {expected_file}; " + "no source was dispatched." ) + def require_result( + self, request: CustomFunctionRegistrationRequest, + result: CustomFunctionRegistrationResult, + ) -> None: + """Require the returned mutation receipt to identify the admitted owner.""" + if result.server_identity != self.server_identity or result.connection != request.connection: + raise ValueError("Registration returned a different execution owner.") + if result.persisted != request.persist: + raise ValueError("Registration returned a different persistence policy.") + if request.persist and ( + result.storage_dir != self.storage_dir + or result.source_file_paths != (self.source_file_path,) + ): + raise ValueError("Registration returned a different persistence destination.") + class FunctionParameterSource(str, Enum): """Nominal source of one callable parameter in agent-facing docs.""" @@ -527,6 +567,7 @@ class CustomFunctionRegistrationResult(AgentResultEnvelope): functions: tuple[FunctionCatalogEntry, ...] = () next_steps: tuple[str, ...] = () connection: ExecutionConnectionSpec = field(default_factory=ExecutionConnectionSpec) + server_identity: ProcessIdentity | None = None class CustomFunctionRegistrationControlResponse( diff --git a/openhcs/agent/services/endpoint_function_catalog_service.py b/openhcs/agent/services/endpoint_function_catalog_service.py index 09ba916955..7db9fc626f 100644 --- a/openhcs/agent/services/endpoint_function_catalog_service.py +++ b/openhcs/agent/services/endpoint_function_catalog_service.py @@ -45,6 +45,7 @@ def __init__(self, request: CustomFunctionRegistrationRequest): super().__init__( f"Registration observation failed after invoking {request.connection.transport_endpoint()}; " f"destination={request.storage_dir}, function={request.function_name!r}. " + f"Selected server={request.server_identity!r}. " "Outcome is uncertain; source or registry mutation may have completed." ) @@ -383,24 +384,26 @@ def register_custom_function( if request.persist: self._path_policy.assert_writable(destination.storage_dir) self._path_policy.assert_writable(destination.source_file_path) + request = replace(request, server_identity=destination.server_identity) self.invalidate() self._config_provider = lambda: endpoint try: result = client.register_custom_function(request) - except (TimeoutError, ConnectionError, OSError) as error: - raise CustomFunctionRegistrationUncertainError(request) from error - if not request.persist: - from openhcs.processing.custom_functions.manager import ( - CustomFunctionManager, - ) + destination.require_result(request, result) + if not request.persist: + from openhcs.processing.custom_functions.manager import ( + CustomFunctionManager, + ) - CustomFunctionManager(create_storage=False).register_from_code( - request.source_code, - persist=False, - clear_caches=False, - emit_signal=False, - ) - self.invalidate() + CustomFunctionManager(create_storage=False).register_from_code( + request.source_code, + persist=False, + clear_caches=False, + emit_signal=False, + ) + self.invalidate() + except Exception as error: + raise CustomFunctionRegistrationUncertainError(request) from error return result def invalidate(self) -> None: diff --git a/openhcs/agent/services/function_catalog_service.py b/openhcs/agent/services/function_catalog_service.py index e8a8e55e66..1bd0c54766 100644 --- a/openhcs/agent/services/function_catalog_service.py +++ b/openhcs/agent/services/function_catalog_service.py @@ -547,16 +547,23 @@ def register_custom_function( self, request: CustomFunctionRegistrationRequest ) -> CustomFunctionRegistrationResult: request = request.admitted(request.admission_policy or self._path_policy) + # Caller admission cannot enlarge this server's own policy. + request.admitted(self._path_policy) + server_identity = request.require_server_identity() manager = custom_function_manager.CustomFunctionManager(create_storage=False) if request.persist and manager.storage_dir.resolve(strict=False) != Path(request.storage_dir).resolve(strict=False): raise ValueError( f"Selected endpoint owns custom storage {manager.storage_dir}, " f"not requested {request.storage_dir}; no source was evaluated." ) + def admit_write(path: Path) -> Path: + self._path_policy.assert_writable(path) + return request.admission_policy.assert_writable(path) + registered_functions = manager.register_from_code( request.source_code, persist=request.persist, expected_function_name=request.function_name, - write_admission=request.admission_policy.assert_writable, + write_admission=admit_write, ) function_ids = self.function_ids_for_callables(tuple(registered_functions)) entries = tuple( @@ -593,6 +600,7 @@ def register_custom_function( ) ), connection=request.connection, + server_identity=server_identity, ) def custom_function_registration_destination( diff --git a/openhcs/mcp/dev_client_commands/knowledge_pipeline.py b/openhcs/mcp/dev_client_commands/knowledge_pipeline.py index 34a981f835..4c9a92af61 100644 --- a/openhcs/mcp/dev_client_commands/knowledge_pipeline.py +++ b/openhcs/mcp/dev_client_commands/knowledge_pipeline.py @@ -4,6 +4,7 @@ import argparse from collections.abc import Mapping +from dataclasses import fields from pathlib import Path from typing import ClassVar @@ -14,7 +15,11 @@ from openhcs.agent.dto.authoring import AuthoringContextRequest from openhcs.agent.dto.common import JsonObject, JsonValue from openhcs.agent.dto.execution import ExecutionConnectionSpec -from openhcs.agent.dto.functions import FunctionDetailRequest, FunctionSearchRequest +from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationRequest, + FunctionDetailRequest, + FunctionSearchRequest, +) from openhcs.agent.dto.pipeline import ( PipelineSourceRenderRequest, PipelineValidationRequest, @@ -28,6 +33,7 @@ McpDevToolCall, McpToolArgumentAuthority, add_pipeline_source_options, + add_request_factory_option, call_mcp_tool, execute_source_session_tool_arguments, execute_source_submit_timeout_seconds, @@ -200,7 +206,7 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "--no-persist", action="store_true", - help="Register for this MCP process only; do not write to the custom function directory.", + help="Register on the explicitly selected execution endpoint without persisting a source file.", ) parser.add_argument( "--full-signature", @@ -208,6 +214,16 @@ def configure_parser(self, parser: argparse.ArgumentParser) -> None: action="store_false", default=True, ) + for connection_field in fields(ExecutionConnectionSpec): + add_request_factory_option( + parser, CustomFunctionRegistrationRequest.from_fields, + connection_field.name, f"--{connection_field.name.replace('_', '-')}", + ) + for field_name in ("function_name", "storage_dir"): + add_request_factory_option( + parser, CustomFunctionRegistrationRequest.from_fields, + field_name, f"--{field_name.replace('_', '-')}", + ) parser.add_argument( "--json", action="store_true", @@ -221,11 +237,19 @@ def tool_arguments( source_code = args.source_code if source_code is None: source_code = Path(args.source_file).read_text(encoding="utf-8") + connection = ExecutionConnectionSpec(**{ + connection_field.name: vars(args)[connection_field.name] + for connection_field in fields(ExecutionConnectionSpec) + }) + connection.require_port("Custom function registration") return McpToolArgumentAuthority.from_payload( { "source_code": source_code, "persist": not args.no_persist, "compact_signature": args.compact_signature, + "function_name": args.function_name, + "storage_dir": args.storage_dir, + **connection.tool_arguments(), } ) diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 2c28adee51..8aef01e8c4 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -70,6 +70,8 @@ if TYPE_CHECKING: from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationDestination, + CustomFunctionRegistrationDestinationRequest, CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogControlRequest, @@ -911,7 +913,9 @@ def register_custom_function( response ).result - def custom_function_registration_destination(self, request): + def custom_function_registration_destination( + self, request: CustomFunctionRegistrationDestinationRequest, + ) -> CustomFunctionRegistrationDestination: """Require the selected endpoint's native admission contract before mutation.""" from openhcs.agent.dto.functions import ( CustomFunctionRegistrationDestinationControlResponse, diff --git a/tests/unit/agent/test_custom_registration_admission.py b/tests/unit/agent/test_custom_registration_admission.py index b2bf76867a..3bfd16a6d1 100644 --- a/tests/unit/agent/test_custom_registration_admission.py +++ b/tests/unit/agent/test_custom_registration_admission.py @@ -1,10 +1,14 @@ """Built-in destination and route admission before custom source side effects.""" +import asyncio +import json from dataclasses import replace from pathlib import Path +from types import SimpleNamespace import pytest from zmqruntime.config import TransportMode +from zmqruntime.messages import ProcessIdentity from openhcs.agent.dto.common import SCHEMA_VERSION from openhcs.agent.dto.functions import ( @@ -20,15 +24,23 @@ ) from openhcs.agent.services.function_catalog_service import FunctionCatalogService from openhcs.processing.custom_functions.manager import CustomFunctionManager +from openhcs.processing.custom_functions.runtime_registry import ( + CustomFunctionRuntimeRegistry, +) from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG def request(root, **changes): - return replace(CustomFunctionRegistrationRequest.from_fields( - source_code="@numpy\ndef boundary_probe(image):\n return image\n", - function_name="boundary_probe", storage_dir=str(root), port=15993, - transport_mode=TransportMode.TCP, - ), **changes) + return replace( + CustomFunctionRegistrationRequest.from_fields( + source_code="@numpy\ndef boundary_probe(image):\n return image\n", + function_name="boundary_probe", + storage_dir=str(root), + port=15993, + transport_mode=TransportMode.TCP, + ), + **changes, + ) def policy(root): @@ -37,10 +49,15 @@ def policy(root): def test_missing_explicit_route_rejects_before_client_creation(tmp_path): made = [] - catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, - client_factory=lambda endpoint: made.append(endpoint), path_policy=policy(tmp_path)) + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: made.append(endpoint), + path_policy=policy(tmp_path), + ) with pytest.raises(ValueError, match="explicit port"): - catalog.register_custom_function(CustomFunctionRegistrationRequest(source_code="not evaluated")) + catalog.register_custom_function( + CustomFunctionRegistrationRequest(source_code="not evaluated") + ) assert not made @@ -59,37 +76,57 @@ def test_write_escape_rejects_before_endpoint_dispatch(tmp_path, escape): root.mkdir() (root / sentinel.name).symlink_to(sentinel) made = [] - catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, - client_factory=lambda endpoint: made.append(endpoint), path_policy=policy(owned)) + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: made.append(endpoint), + path_policy=policy(owned), + ) with pytest.raises(AgentPathPolicyError): catalog.register_custom_function(request(root)) assert not made assert sentinel.read_text() == "preserve" -@pytest.mark.parametrize("wrong_store, timeout", ((False, False), (True, False), (False, True))) +@pytest.mark.parametrize( + "wrong_store, timeout", ((False, False), (True, False), (False, True)) +) def test_exact_owned_route_and_store_precede_mutation(tmp_path, wrong_store, timeout): root = tmp_path / "custom" mutations = [] endpoints = [] + class Client: def custom_function_registration_destination(self, probe): assert probe.function_name == "boundary_probe" native = tmp_path / "foreign" if wrong_store else root - return CustomFunctionRegistrationDestination(str(native), str(native / "boundary_probe.py")) + return CustomFunctionRegistrationDestination( + str(native), str(native / "boundary_probe.py") + ) + def register_custom_function(self, admitted): mutations.append(admitted) assert admitted.admission_policy == policy(tmp_path) + assert admitted.server_identity == ProcessIdentity.current() if timeout: raise TimeoutError("controlled post-dispatch observation") - return CustomFunctionRegistrationResult(schema_version=SCHEMA_VERSION, connection=admitted.connection) + return CustomFunctionRegistrationResult( + schema_version=SCHEMA_VERSION, + connection=admitted.connection, + server_identity=admitted.server_identity, + storage_dir=str(root), + source_file_paths=(str(root / "boundary_probe.py"),), + ) + def disconnect(self): pass + def factory(endpoint): endpoints.append(endpoint) return Client() - catalog = ZMQFunctionCatalogService(lambda: OPENHCS_ZMQ_CONFIG, - client_factory=factory, path_policy=policy(tmp_path)) + + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, client_factory=factory, path_policy=policy(tmp_path) + ) if wrong_store: with pytest.raises(ValueError, match="no source was dispatched"): catalog.register_custom_function(request(root)) @@ -111,24 +148,267 @@ def factory(endpoint): def test_native_destination_query_does_not_create_storage(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "absent")) - result = FunctionCatalogService(path_policy=policy(tmp_path)).custom_function_registration_destination( - CustomFunctionRegistrationDestinationRequest(function_name="boundary_probe")) + result = FunctionCatalogService( + path_policy=policy(tmp_path) + ).custom_function_registration_destination( + CustomFunctionRegistrationDestinationRequest(function_name="boundary_probe") + ) assert Path(result.storage_dir) == CustomFunctionManager.default_storage_directory() - assert Path(result.source_file_path) == CustomFunctionManager.source_path_for_name(Path(result.storage_dir), "boundary_probe") + assert Path(result.source_file_path) == CustomFunctionManager.source_path_for_name( + Path(result.storage_dir), "boundary_probe" + ) assert not (tmp_path / "absent").exists() def test_local_denial_precedes_manager_evaluation_and_creation(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "foreign")) evaluated = [] - monkeypatch.setattr(CustomFunctionManager, "_prepare_source", lambda *args: evaluated.append(args)) + monkeypatch.setattr( + CustomFunctionManager, "_prepare_source", lambda *args: evaluated.append(args) + ) root = CustomFunctionManager.default_storage_directory() with pytest.raises(AgentPathPolicyError): - FunctionCatalogService(path_policy=policy(tmp_path / "owned")).register_custom_function(request(root)) + FunctionCatalogService( + path_policy=policy(tmp_path / "owned") + ).register_custom_function(request(root)) assert not evaluated assert not (tmp_path / "foreign").exists() +@pytest.mark.parametrize("escape", ("ordinary", "ancestor", "destination")) +def test_native_escape_denial_preserves_registry_and_sentinel( + tmp_path, monkeypatch, escape +): + owned = tmp_path / "owned" + outside = tmp_path / "outside" + owned.mkdir() + outside.mkdir() + sentinel = outside / "boundary_probe.py" + sentinel.write_text("preserve native sentinel") + data_home = outside if escape == "ordinary" else owned + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + root = CustomFunctionManager.default_storage_directory() + if escape == "ancestor": + root.parent.mkdir() + root.symlink_to(outside, target_is_directory=True) + elif escape == "destination": + root.mkdir(parents=True) + (root / sentinel.name).symlink_to(sentinel) + before = CustomFunctionRuntimeRegistry.metadata_by_name() + evaluated = [] + monkeypatch.setattr( + CustomFunctionManager, "_prepare_source", lambda *args: evaluated.append(args) + ) + with pytest.raises(AgentPathPolicyError): + FunctionCatalogService(path_policy=policy(owned)).register_custom_function( + request(root, server_identity=ProcessIdentity.current()) + ) + assert not evaluated + assert CustomFunctionRuntimeRegistry.metadata_by_name() == before + assert sentinel.read_text() == "preserve native sentinel" + + def test_public_factory_cannot_expand_write_authority(tmp_path): with pytest.raises(TypeError, match="admission_policy"): - CustomFunctionRegistrationRequest.from_fields(source_code="", admission_policy=policy(tmp_path)) + CustomFunctionRegistrationRequest.from_fields( + source_code="", admission_policy=policy(tmp_path) + ) + with pytest.raises(TypeError, match="server_identity"): + CustomFunctionRegistrationRequest.from_fields( + source_code="", server_identity=ProcessIdentity.current() + ) + + +@pytest.mark.parametrize( + "identity", (None, replace(ProcessIdentity.current(), create_time=0)) +) +def test_changed_or_missing_native_owner_rejects_before_evaluation( + tmp_path, monkeypatch, identity +): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + evaluated = [] + monkeypatch.setattr( + CustomFunctionManager, "_prepare_source", lambda *args: evaluated.append(args) + ) + root = CustomFunctionManager.default_storage_directory() + with pytest.raises(ValueError, match="no source was evaluated"): + FunctionCatalogService(path_policy=policy(tmp_path)).register_custom_function( + request(root, server_identity=identity) + ) + assert not evaluated + assert not root.exists() + + +def test_caller_admission_cannot_expand_native_server_policy(tmp_path, monkeypatch): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + root = CustomFunctionManager.default_storage_directory() + admitted = request(root, server_identity=ProcessIdentity.current()).admitted( + policy(tmp_path) + ) + with pytest.raises(AgentPathPolicyError): + FunctionCatalogService( + path_policy=policy(tmp_path / "restricted") + ).register_custom_function(admitted) + assert not root.exists() + + +def test_native_destination_file_must_match_admitted_name(tmp_path): + destination = CustomFunctionRegistrationDestination( + str(tmp_path), str(tmp_path / "other.py") + ) + with pytest.raises(ValueError, match="no source was dispatched"): + destination.require_request(request(tmp_path)) + + +def test_wrong_owner_mutation_receipt_is_uncertain_not_replayed(tmp_path): + mutations = [] + + class Client: + def custom_function_registration_destination(self, probe): + return CustomFunctionRegistrationDestination( + str(tmp_path), str(tmp_path / "boundary_probe.py") + ) + + def register_custom_function(self, admitted): + mutations.append(admitted) + return CustomFunctionRegistrationResult( + schema_version=SCHEMA_VERSION, + connection=admitted.connection, + server_identity=replace(ProcessIdentity.current(), create_time=0), + ) + + def disconnect(self): + pass + + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: Client(), + path_policy=policy(tmp_path), + ) + try: + with pytest.raises( + CustomFunctionRegistrationUncertainError, match="Selected server" + ): + catalog.register_custom_function(request(tmp_path)) + assert len(mutations) == 1 + assert catalog._config_provider().default_port == 15993 + finally: + catalog.close() + + +def test_registration_cli_projects_declarations_and_rejects_missing_route(tmp_path): + from openhcs.mcp.dev_client import _build_parser, _calls_from_args + from openhcs.mcp.dev_client_core import McpDevCliUsageError + + parser = _build_parser() + missing = parser.parse_args(("register-custom-function", "--source-code", "source")) + with pytest.raises(McpDevCliUsageError, match="explicit port"): + _calls_from_args(missing) + args = parser.parse_args( + ( + "register-custom-function", + "--source-code", + "source", + "--port", + "15993", + "--storage-dir", + str(tmp_path), + "--function-name", + "boundary_probe", + "--transport-mode", + "tcp", + ) + ) + (call,) = _calls_from_args(args) + assert call.name == "openhcs_register_custom_function" + assert call.arguments["port"] == 15993 + assert call.arguments["transport_mode"] == "tcp" + assert call.arguments["storage_dir"] == str(tmp_path) + assert call.arguments["function_name"] == "boundary_probe" + assert "server_identity" not in call.arguments + + +def test_manager_admitted_persistence_uses_existing_registry_and_source_owner( + tmp_path, monkeypatch +): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + for name in ( + "_declarations_by_name", + "_published_exports", + "_preparation_outcomes", + "_preparation_threads", + ): + monkeypatch.setattr(CustomFunctionRuntimeRegistry, name, {}) + monkeypatch.setattr(CustomFunctionRuntimeRegistry, "_source_revision", None) + manager = CustomFunctionManager(create_storage=False) + try: + [function] = manager.register_from_code( + request(manager.storage_dir).source_code, + expected_function_name="boundary_probe", + write_admission=policy(tmp_path).assert_writable, + clear_caches=False, + emit_signal=False, + ) + source = manager.source_path_for_function(function) + assert source == manager.source_path_for_name( + manager.storage_dir, "boundary_probe" + ) + assert source.read_text() == request(manager.storage_dir).source_code + assert ( + CustomFunctionRuntimeRegistry.metadata_by_name()["boundary_probe"].func + is function + ) + import numpy as np + + image = np.arange(4).reshape(2, 2) + np.testing.assert_array_equal(function(image), image) + CustomFunctionRuntimeRegistry.clear() + from openhcs.processing.custom_functions import boundary_probe + + assert boundary_probe.__module__ == "openhcs.processing.custom_functions" + np.testing.assert_array_equal(boundary_probe(image), image) + finally: + CustomFunctionRuntimeRegistry.clear() + + +@pytest.mark.parametrize("missing_route", (False, True)) +def test_real_generated_mcp_boundary_excludes_authority_and_denies_before_client( + tmp_path, missing_route +): + from openhcs.mcp.server import build_server + + made = [] + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: made.append(endpoint), + path_policy=policy(tmp_path / "owned"), + ) + + async def invoke(): + built = build_server(SimpleNamespace(function_catalog=catalog)) + tool = next( + tool + for tool in await built.list_tools() + if tool.name == "openhcs_register_custom_function" + ) + properties = tool.inputSchema["properties"] + assert {"port", "host", "storage_dir", "function_name"} <= properties.keys() + assert {"admission_policy", "server_identity"}.isdisjoint(properties) + arguments = { + "source_code": "not evaluated", + "function_name": "boundary_probe", + "storage_dir": str(tmp_path / "outside"), + } + if not missing_route: + arguments["port"] = 15993 + return await built.call_tool("openhcs_register_custom_function", arguments) + + result = asyncio.run(invoke()) + content = result[0] if isinstance(result, tuple) else result.content + payload = json.loads(content[0].text) + error = payload["errors"][0] + assert error["code"] == ( + "mcp_tool_failed" if missing_route else "agent_path_policy_rejected" + ) + assert not made + catalog.close() diff --git a/tests/unit/agent/test_mcp_server.py b/tests/unit/agent/test_mcp_server.py index 00db35d779..516adb0339 100644 --- a/tests/unit/agent/test_mcp_server.py +++ b/tests/unit/agent/test_mcp_server.py @@ -4261,6 +4261,8 @@ def test_mcp_dev_client_function_commands_project_tool_arguments(tmp_path): str(custom_source), "--no-persist", "--full-signature", + "--port", + "15993", ) ) authoring_args = parser.parse_args( @@ -4290,6 +4292,12 @@ def test_mcp_dev_client_function_commands_project_tool_arguments(tmp_path): "source_code": "from openhcs.core.memory import numpy\n", "persist": False, "compact_signature": False, + "function_name": None, + "storage_dir": None, + "host": "localhost", + "port": 15993, + "transport_mode": None, + "persistent": True, } assert authoring_call.name == "openhcs_get_authoring_context" assert authoring_call.arguments == { diff --git a/tests/unit/test_custom_function_lifecycle.py b/tests/unit/test_custom_function_lifecycle.py index 29a1be41f1..70163268db 100644 --- a/tests/unit/test_custom_function_lifecycle.py +++ b/tests/unit/test_custom_function_lifecycle.py @@ -311,7 +311,7 @@ def pause_reconstruction(self, declared, contract): def isolated_custom_runtime(monkeypatch, tmp_path): storage_dir = tmp_path / "custom_functions" storage_dir.mkdir() - monkeypatch.setattr(manager_module, "get_data_file_path", lambda _name: storage_dir) + monkeypatch.setattr(manager_module, "get_data_file_path", lambda _name, *, create=True: storage_dir) monkeypatch.setattr(CustomFunctionRuntimeRegistry, "_declarations_by_name", {}) monkeypatch.setattr(CustomFunctionRuntimeRegistry, "_published_exports", {}) monkeypatch.setattr(CustomFunctionRuntimeRegistry, "_preparation_outcomes", {}) diff --git a/tests/unit/test_function_catalog_zmq.py b/tests/unit/test_function_catalog_zmq.py index 388e64f71a..b8f8e67361 100644 --- a/tests/unit/test_function_catalog_zmq.py +++ b/tests/unit/test_function_catalog_zmq.py @@ -15,6 +15,8 @@ from openhcs.agent.dto.functions import ( CustomFunctionRegistrationControlResponse, + CustomFunctionRegistrationDestinationControlResponse, + CustomFunctionRegistrationDestinationRequest, CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogControlPayload, @@ -530,6 +532,27 @@ def test_zmq_router_registers_custom_source_through_catalog_owner( assert observed_requests == [request] +def test_native_registration_destination_bypasses_catalog_preparation( + tmp_path, monkeypatch, +) -> None: + from zmqruntime.messages import ProcessIdentity + + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "absent")) + context = _context(Future()) + def reject_preparation(): + raise AssertionError("Destination admission cannot prepare the function catalog") + monkeypatch.setattr(context.function_catalog_preparation, "ensure_started", reject_preparation) + response = ZMQControlMessageRouter.handle( + FunctionCatalogControlPayload.from_request( + CustomFunctionRegistrationDestinationRequest(function_name="boundary_probe") + ).to_dict(), context, + ) + destination = CustomFunctionRegistrationDestinationControlResponse.from_control_response(response).destination + assert destination.server_identity == ProcessIdentity.current() + assert destination.source_file_path == str(tmp_path / "absent" / "openhcs" / "custom_functions" / "boundary_probe.py") + assert not (tmp_path / "absent").exists() + + def test_zmq_router_delegates_search_to_catalog_owner(monkeypatch) -> None: catalog = _catalog() observed = [] diff --git a/tests/unit/test_function_step_transport.py b/tests/unit/test_function_step_transport.py index a2f16d8c72..b50344fffb 100644 --- a/tests/unit/test_function_step_transport.py +++ b/tests/unit/test_function_step_transport.py @@ -473,7 +473,7 @@ def codex_lazy_import_probe(image): encoding="utf-8", ) vars(custom_functions).pop(func_name, None) - monkeypatch.setattr(manager_module, "get_data_file_path", lambda _name: storage_dir) + monkeypatch.setattr(manager_module, "get_data_file_path", lambda _name, *, create=True: storage_dir) namespace: dict[str, object] = {} try: From 1503466660b6bf4d36028a950ce41d35e1d3803a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:33:59 -0400 Subject: [PATCH 03/13] Record integrated custom registration evidence and pending live gates --- .../custom_registration_admission_20260929.md | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 028e0b1899..2dabff5a2f 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -2,7 +2,10 @@ Implementation owner: Lovelace; integration/live scheduling owner: coordinator. Source tree: `/home/ts/wt/openhcs-custom-function-admission-20260929`. -Base: current `openhcsdev/main` 6c2bc4671, integrated normally. +Initial base: `openhcsdev/main` 6c2bc4671. Subsequent normal integration of +main250fd1ecf (#234) is merge5899d6323; recorded PolyStore1209068 is checked out +only in this worker's isolated submodule. Implementation commits625e82fce and +4ffc2b202 precede that merge. No frozen installed source is edited. ## Verified boundary and checkpoint @@ -47,7 +50,7 @@ field exclusion. `git diff --check` and new-test Ruff checks pass. Whole touched source Ruff is not claimed clean: existing unrelated findings remain. Follow-up evidence: **94 tests passed, two plugin-disabled configuration warnings, -10.17 seconds, peak RSS 322984 KiB**. This runs the new admission file, existing +10.41 seconds, peak RSS 323352 KiB**. This runs the new admission file, existing custom-function lifecycle and function-catalog ZMQ suites, and three affected MCP/CLI regressions. It adds native ordinary/ancestor/file-symlink denial with unchanged registry membership and sentinel bytes, PID-incarnation rejection, @@ -77,6 +80,63 @@ Owned disposable fixtures: No MCP/JVM/GUI handle was started. Tests are source evidence, not installed or live acceptance, and the frozen installed 17/228 tree remains untouched. +Following #234 integration, isolated source environments explicitly pin +`POLYSTORE_IMAGEJ_CACHE_ROOT=/home/ts/.cache/polystore/imagej` and +`POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD=false` before imports. This is not a request +to download, copy or launch a bundle; no native runtime is started by these +admission tests. + +At merge5899d6323, with recorded PolyStore1209068 and the explicit cache policy, +the post-integration check runs only the22 admission cases plus the real native +control-router destination case: **23 passed, 6.18 seconds, peak287876 KiB**. +OpenHCS and PolyStore import paths resolve inside this isolated tree. AST parsing +of all15 changed Python source/test files passes; this is a focused source check, +not a full NRA scan or a global proof. + +Source commands used the shared Python executable, explicit worktree/submodule +PYTHONPATH, `OPENHCS_CPU_ONLY=true`, `PYTHONDONTWRITEBYTECODE=1`, and +`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`. Import OpenHCS before `pytest.main`; pass +`--noconftest --import-mode=importlib -p no:cacheprovider -o addopts= -q --tb=short` +and a basetemp below the owned scratch directory. The94-test selectors were: + +```text +tests/unit/agent/test_custom_registration_admission.py +tests/unit/test_custom_function_lifecycle.py +tests/unit/test_function_catalog_zmq.py +tests/unit/agent/test_mcp_server.py::test_mcp_register_custom_function_delegates_to_function_catalog_service +tests/unit/agent/test_mcp_server.py::test_mcp_dev_client_function_commands_project_tool_arguments +tests/unit/agent/test_mcp_server.py::test_mcp_dev_client_register_custom_function_renders_next_steps +``` + +All lightweight check handles are terminal; this worker has never acquired a +heavy runtime slot for230. No optional hosted CI is blocking the checkpoint. + +## Changed paths + +Production: + +- `openhcs/agent/capabilities.py` +- `openhcs/agent/dto/functions.py` +- `openhcs/agent/services/endpoint_function_catalog_service.py` +- `openhcs/agent/services/function_catalog_service.py` +- `openhcs/core/xdg_paths.py` +- `openhcs/mcp/context.py` +- `openhcs/mcp/dev_client_commands/knowledge_pipeline.py` +- `openhcs/processing/custom_functions/manager.py` +- `openhcs/runtime/zmq_control.py` +- `openhcs/runtime/zmq_execution_client.py` + +Tests: + +- `tests/unit/agent/test_custom_registration_admission.py` +- `tests/unit/agent/test_mcp_server.py` +- `tests/unit/test_custom_function_lifecycle.py` +- `tests/unit/test_function_catalog_zmq.py` +- `tests/unit/test_function_step_transport.py` (fixture projection only; module + collection blocked as recorded above) + +Receipt/body: `docs/plans/custom_registration_admission_20260929.md`. + ## Actual pattern review (focused source, not a full NRA proof) - BOUND-2/BOUND-6: existing `AgentPathPolicy` owns write permission; From f043ac499baf48e98e3ccbb71d8acd7dfe9e69eb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:36:44 -0400 Subject: [PATCH 04/13] Retain registration owner coordination and scratch disposition --- .../custom_registration_205_coordination.md | 30 +++++++++++++++++++ .../custom_registration_admission_20260929.md | 11 ++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/plans/custom_registration_205_coordination.md diff --git a/docs/plans/custom_registration_205_coordination.md b/docs/plans/custom_registration_205_coordination.md new file mode 100644 index 0000000000..029b0a5d7f --- /dev/null +++ b/docs/plans/custom_registration_205_coordination.md @@ -0,0 +1,30 @@ +Zeno (#205 DTO/service owner): Lovelace's separately assigned #230 checkpoint is +draft https://github.com/OpenHCSDev/openhcs/pull/233 at150346666, normally integrated +with main250fd1ecf / recorded PolyStore1209068. Parent requested direct shared-file +coordination; this note identifies the exact existing-owner changes, not another +catalog implementation. + +Shared `openhcs/agent/dto/functions.py` scope is the custom-registration request, +destination query/response/result and control-message member. Shared +`openhcs/agent/services/function_catalog_service.py` scope is constructor path +policy injection, `register_custom_function`, and read-only +`custom_function_registration_destination`. Function parameter/artifact-selector +metadata and your measurement/runtime contracts are untouched by this task. + +Existing `AgentPathPolicy`, `ExecutionConnectionSpec`, `ProcessIdentity` and +`CustomFunctionManager` own admission, routing, PID incarnation and native path +derivation. No new registry or provider. Both caller and native policy are checked +before evaluation/persistence; public MCP fields cannot supply write authority or +the server identity. Post-dispatch exceptions/receipt mismatch stay uncertain, +with no replay/fallback. The original H002 receipt033 and persisted side effect +are preserved and never reused as a fixture. + +94 affected source tests pass; after normal main integration23 focused cases +pass. A separate transport-module collection remains blocked by its unbuilt +CellProfiler native extension. Actual stdio owned-vs-shared, delayed-response, +compile/execute acceptance is pending coordinator's finite technical slot; no +installed edit/JVM/new runtime has been performed. + +Please flag any in-progress changes to these registration-specific regions or +their constructors before further shared-owner edits; maintain the single +function-catalog authority. Parent retains integration ownership for both PRs. diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 2dabff5a2f..0ba570af90 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -75,8 +75,11 @@ the 94-test evidence above, not a claim that this missing dependency disappeared Available RAM stayed above 8 GiB (latest13.4 GiB); guard still warns about historical11.1 GiB swap. No heavy runtime or validation-lock holder was created. -Owned disposable fixtures: +Owned disposable fixtures (now removed after all handles were terminal): `/home/ts/.cache/agent-scratch/openhcs-registration-230-20260929/pytest`. +The final owned parent directory contained3.5 MiB of synthetic pytest fixtures; +it was deleted after retaining these results. Fixtures can be regenerated from +the tests. Source, receipts, original033 and the foreign saved file are preserved. No MCP/JVM/GUI handle was started. Tests are source evidence, not installed or live acceptance, and the frozen installed 17/228 tree remains untouched. @@ -136,6 +139,12 @@ Tests: collection blocked as recorded above) Receipt/body: `docs/plans/custom_registration_admission_20260929.md`. +Shared-owner coordination: `docs/plans/custom_registration_205_coordination.md`, +posted directly to the existing #205 owner at +https://github.com/OpenHCSDev/openhcs/pull/205#issuecomment-5900395883. +The local direct-DM route did not resolve this worker; no thread registration, +bus repair, agent restart or goal replay was attempted. Coordination scope is +the registration-specific DTO/service regions, not measurement declarations. ## Actual pattern review (focused source, not a full NRA proof) From 9020cae186aa79a3460675f527a3826ef1f1f531 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:57:55 -0400 Subject: [PATCH 05/13] Complete registration filename ownership and explicit-route authoring workflow --- .../custom_registration_admission_20260929.md | 27 +- .../mcp_knowledge_base_manifest.json | 2 +- .../processing/custom_functions/manager.py | 12 +- .../references/custom-function-authoring.md | 88 +- .../references/measurement-interpretation.md | 12 +- .../check_custom_registration_live.py | 769 ++++++++++++++++++ .../diagnostics/registration_probe_source.py | 8 + .../test_custom_registration_admission.py | 36 + 8 files changed, 916 insertions(+), 38 deletions(-) create mode 100644 tests/diagnostics/check_custom_registration_live.py create mode 100644 tests/diagnostics/registration_probe_source.py diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 0ba570af90..454222fdec 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -152,8 +152,13 @@ the registration-specific DTO/service regions, not measurement declarations. `CustomFunctionManager` and XDG helpers own paths. A different native storage declaration changes that owner, not a new endpoint-to-filename registry. - IMPL-12: replaced the manager's repeated name-to-file construction with its - `source_path_for_name`, consumed by admission and actual save. New public names - take the same helper, not a second source parser. + `source_path_for_name`, consumed by admission, save, named load/read/delete, + revision checking (`require_source`) and both rename destinations. The six + remaining same-schema constructions were removed in the pre-live follow-up. + Five owner-interception cases witness that those named operations cannot + bypass it. Directory enumeration still reads actual persisted paths; it is + not name-to-file construction and has not been replaced by a filename roster. + New public names take the same helper, not a second source parser. - MEMB-2/MEMB-3: the new control action declares its message on the existing request/strategy family. Existing registration/discovery registries remain derived; no companion roster, new runner or duplicate custom registry. @@ -170,6 +175,24 @@ the registration-specific DTO/service regions, not measurement declarations. ## Remaining acceptance (not closed) +The current normal merge includes mainfbf6b2d91 (#225/#236), merge8fc0ecc63. +The authoring how-to now requires explicit reflected routing, deliberately +authored name and a caller-intended absolute store obtained through the native +Manager in the controlled server launch environment. It describes pre-dispatch +destination/incarnation proof and postdispatch uncertainty separately; a +receipt after writing is not admission. No arbitrary-server MCP destination +lookup is claimed. The existing knowledge manifest summary is updated; it +contains no checked-in document digest to duplicate. The empirical guide now +records parent's merged/installed #225 synthetic acceptance, not biology. + +`tests/diagnostics/check_custom_registration_live.py` is a reusable bounded +ordinary stdio MCP/native server journey, with a test-only delayed-response and +unsupported-proof seam through the existing server launch owner. It pins all +source imports, controlled XDG roots, shared-cache/download-false policy and +one native thread; acquires the shared nonblocking lock and records measured +call durations (not session ages), sentinels, original inputs and handles. +Unexpected calls are not retried. This prepared driver is not a passing receipt. + After verified closure of the frozen author's owned MCP/viewer and release of the technical slot, run controlled owned-vs-shared endpoint, escaping-path and delayed-response checks, plus register/discover/compile/execute on a tiny diff --git a/docs/source/development/mcp_knowledge_base_manifest.json b/docs/source/development/mcp_knowledge_base_manifest.json index f6591c2a9e..e225663960 100644 --- a/docs/source/development/mcp_knowledge_base_manifest.json +++ b/docs/source/development/mcp_knowledge_base_manifest.json @@ -822,7 +822,7 @@ "document_id": "openhcs_custom_function_workflow", "section_count": 0, "source_path": "packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md", - "summary": "Add a missing analysis operation through existing typed custom-function owners: discover contracts, scope catalog and persistence, verify fresh-process imports, compile artifacts, inspect bounded outputs and separate technical reuse from biological recipe acceptance.", + "summary": "Add a missing operation through typed custom-function owners: explicit endpoint/name/store admission before dispatch, controlled-launch native destination proof, postdispatch uncertainty without replay, stable imports, compilation and bounded outputs; separate technical reuse from biological acceptance.", "tags": ["missing analysis operation", "custom function workflow", "typed artifact authoring", "isolated catalog endpoint", "persisted custom import", "registration timeout reconcile", "ColumnarRows", "3D centres"], "title": "Add a missing analysis operation as a typed custom function" }, diff --git a/openhcs/processing/custom_functions/manager.py b/openhcs/processing/custom_functions/manager.py index a668905c58..817cbe7737 100644 --- a/openhcs/processing/custom_functions/manager.py +++ b/openhcs/processing/custom_functions/manager.py @@ -85,7 +85,7 @@ class CustomFunctionManager: def require_source(self, expected: CustomFunctionSource) -> None: """Reject a persisted declaration whose actual source bytes changed.""" current = self._snapshot_source( - self.storage_dir / f"{expected.function_name}.py" + self.source_path_for_name(self.storage_dir, expected.function_name) ) if current is None or current.source != expected: raise RuntimeError( @@ -273,7 +273,7 @@ def load_custom_function( Returns: Number of functions registered from the persisted file. """ - file_path: Path = self.storage_dir / f"{func_name}.py" + file_path = self.source_path_for_name(self.storage_dir, func_name) snapshot = self._snapshot_source(file_path) if snapshot is None: return 0 @@ -325,7 +325,7 @@ def delete_custom_function(self, func_name: str) -> bool: Returns: True if function file was deleted, False if not found """ - file_path: Path = self.storage_dir / f"{func_name}.py" + file_path = self.source_path_for_name(self.storage_dir, func_name) with CustomFunctionRuntimeRegistry.lifecycle(): if not file_path.exists(): @@ -392,7 +392,7 @@ def get_function_code(self, func_name: str) -> str: Raises: ValueError: If function file not found """ - file_path: Path = self.storage_dir / f"{func_name}.py" + file_path = self.source_path_for_name(self.storage_dir, func_name) if not file_path.exists(): raise ValueError(f"Custom function '{func_name}' not found") @@ -419,7 +419,7 @@ def update_custom_function(self, old_name: str, new_code: str) -> str: ValidationError: If new code is invalid OSError: If file operations fail """ - old_file_path = self.storage_dir / f"{old_name}.py" + old_file_path = self.source_path_for_name(self.storage_dir, old_name) old_snapshot = self._snapshot_source(old_file_path) if old_snapshot is None: raise ValueError(f"Custom function '{old_name}' not found") @@ -431,7 +431,7 @@ def update_custom_function(self, old_name: str, new_code: str) -> str: else: metadata = self._prepare_source(new_code) new_name = metadata.original_name - new_file_path = self.storage_dir / f"{new_name}.py" + new_file_path = self.source_path_for_name(self.storage_dir, new_name) temp_path = self._write_temporary_source(new_code) try: with CustomFunctionRuntimeRegistry.lifecycle(): diff --git a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md index a2f3f93493..7593fdf922 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md +++ b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md @@ -49,31 +49,73 @@ the source and intended storage target within the existing mutation authority. Retain exact source bytes and hash, MCP process identity, execution endpoint, registration receipt, returned function ID, import path and persisted paths. -For parallel isolated sessions, verify catalog ownership as well as GUI bridge -ownership. In the current local stdio route, the catalog endpoint is selected -from the UI config cache when the MCP process starts; passing a GUI connection -to a later UI call does not redirect catalog registration. Pin the owned cache -selector (`OPENHCS_UI_CONFIG_CACHE_FILE`) before starting that MCP process, then -verify its endpoint. Use the current reflected contract if this route changes. -Do not resolve a timeout by registering on a peer or default catalog. - -Also verify the persisted-source storage owner. Local custom-function imports -resolve through `CustomFunctionManager.storage_dir`; in an isolated session, -carry the GUI/backend's appropriate `XDG_DATA_HOME` into each consuming MCP -process at startup. The UI cache selects a catalog endpoint, not that local -source directory. Compare the registration receipt's persisted path with the -consumer's storage scope, then prove the stable import on that consumer. -Changing the launch environment does not update an already running process. -Do not copy the source into default storage or re-register it merely to mask -a directory mismatch; reconcile ownership and any uncertain mutation first. +Registration requires an explicit `port`, including for `persist=false`; the +UI cache is not mutation-route authority. Inspect the reflected connection +fields (`host`, `transport_mode`, `persistent`) and supply the intended owned +execution endpoint. For `persist=true`, also supply `function_name` (the public +identifier you deliberately authored) and an absolute `storage_dir`. Do not +infer the name by parsing source or omit the route to use a default server. + +Derive the caller-intended store through its native owner in the **exact launch +environment of the owned execution server**, before starting either process: + +```python +from openhcs.processing.custom_functions.manager import CustomFunctionManager +intended_store = CustomFunctionManager.default_storage_directory() +``` + +This is a non-creating lookup, not a new filename resolver. In a controlled +isolated launch, pin `XDG_DATA_HOME` in that environment and carry it into every +consumer needing stable imports. Retain the returned absolute path and owned +endpoint from the launcher. If the native launch environment is unknown, stop +and obtain that owner contract; do not guess a default path or submit source +to discover where it gets saved. No public MCP destination-discovery tool is +currently exposed for an arbitrary existing server. + +For example, the controlled synthetic acceptance driver supplies the native +owner's path and deliberately authored name, not a receipt-derived permission: + +```python +arguments = { + "source_code": reviewed_source, + "function_name": "registration_live_probe", + "storage_dir": str(intended_store), + "port": owned_port, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": True, +} +``` + +The existing MCP path policy admits the intended directory and owner-derived +file **before any endpoint dispatch**. The service then queries the native +read-only destination owner, verifies its actual store/file and process +incarnation, and binds that proof to the one registration request. Unsupported +destination proof, a different store, or a changed process rejects without +evaluating the source. The execution service independently admits the native +write before evaluation and persistence. Receipt checks after writing verify +the outcome; they are not write admission or a Python sandbox. + +For isolated sessions, also pin `OPENHCS_UI_CONFIG_CACHE_FILE` before MCP +startup and verify the selected catalog. That launch-time selector still owns +initial discovery; passing a later GUI connection does not redirect it. +An admitted registration selects its explicit endpoint for subsequent catalog +reads, including uncertainty reconciliation. The cache does not select the +local source directory. Changing environment variables does not update an +already running process. Verify the receipt's endpoint, process incarnation, +function ID, stable import and source bytes on each actual consuming process. +Do not copy into default storage to mask a mismatch. Use `openhcs_register_custom_function`, which delegates validation, persistence -and registry publication to `CustomFunctionManager`. Choose persistence when -the reviewed pipeline needs a stable import across GUI, backend or fresh worker -processes, after confirming the returned storage directory is in scope. -An ephemeral backend registration does not prove importability in another -process. A timeout leaves mutation outcome uncertain: reconcile the exact name -on the same endpoint before any explicit retry. Creating an existing name is +and registry publication to `CustomFunctionManager`. An ephemeral registration +does not prove importability in another process. After mutation dispatch, a +timeout or invalid receipt returns `custom_function_registration_uncertain`: +it preserves the explicit endpoint, intended store/name and process identity, +and performs no fallback or automatic registration retry. Keep the same MCP +handle and original input. Read-only discovery on that endpoint and inspection +of the admitted source may reconcile publication; a failed observation is not +proof of no mutation. Obtain explicit disposition before any new mutation. +Creating an existing name is not a persistence upgrade; use an exposed lifecycle update route or record the missing capability rather than overwriting its file or mutating private state. If a lifecycle update is unavailable, a deliberately versioned new callable diff --git a/packaging/codex/openhcs/skills/use-openhcs/references/measurement-interpretation.md b/packaging/codex/openhcs/skills/use-openhcs/references/measurement-interpretation.md index c683124c8c..7fbf8b9c0e 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/references/measurement-interpretation.md +++ b/packaging/codex/openhcs/skills/use-openhcs/references/measurement-interpretation.md @@ -72,12 +72,12 @@ measurements, not ground truth or an automatically validated parameter choice. Availability is determined by the **live** capability registry, not this guide. These source contracts accompany [issue221](https://github.com/OpenHCSDev/openhcs/issues/221); -their source-pinned synthetic MCP/viewer path has verified original crop and -relative spacing, empirical values, unchanged raw pixels/presentation and -author-opened MCP bitmaps at 5da177ad9 with PolyStore c16b8fc, including actual -child shared-cache/no-download policy verification. This is not a -physical-calibration claim or installed readiness: parent merge/install and its -affected-entrypoint check remain separate. Both operations are read-only and +PR225 is merged and its installed synthetic MCP/native viewer journey at +OpenHCS fbf6b2d91 / PolyStore1209068 verified a cropped two-channel fixture, +empirical measurement, snapshot and native close. The earlier source proof +also verified unchanged raw pixels/presentation and shared-cache/no-download +child policy. Neither proof establishes physical calibration or biological +accuracy. Both operations are read-only and require a settled, scalar, non-multiscale image route in a native YX 2-D display. Unbound stacks/RGB, ambiguous records, missing axes, sparse padding, nonfinite inputs/pixels and out-of-bounds geometry fail explicitly; no coordinate clamps. diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py new file mode 100644 index 0000000000..1e3a427a01 --- /dev/null +++ b/tests/diagnostics/check_custom_registration_live.py @@ -0,0 +1,769 @@ +"""Bounded real stdio MCP/native execution acceptance for issue230. + +Run only with the explicitly handed-off slot and existing shared interpreter. +The nonblocking flock is held until all owned handles are terminal. Unexpected +dispatch failures retain the same client/servers for operator disposition; +there is no retry, shared7777 connection, install, viewer, JVM or science input. +""" + +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import subprocess +import sys +import time +import traceback +from pathlib import Path + +SOURCE = Path(__file__).resolve().parents[2] +PYTHON = Path("/home/ts/code/projects/openhcs/.venv/bin/python") +LOCK = Path("/home/ts/wt/openhcs-issue-batch-20260929/validation.lock") +PROBE = SOURCE / "tests/diagnostics/registration_probe_source.py" + + +def child_server(role: str, audit: Path, native_argv: list[str]) -> None: + """Diagnostic fault seam on the real launcher/server, not another registry.""" + from zmqruntime.messages import ControlErrorResponse, MessageFields + + from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationControlResponse, + FunctionCatalogControlField, + FunctionCatalogControlMessageType, + ) + from openhcs.runtime.zmq_execution_server import ZMQExecutionServer + from openhcs.runtime.zmq_execution_server_launcher import main + + class DiagnosticExecutionServer(ZMQExecutionServer): + _server_type = "registration_acceptance_fixture" + + def handle_control_message(self, message): + with audit.open("a") as stream: + stream.write( + json.dumps( + { + "type": message[MessageFields.TYPE], + "time": time.time(), + } + ) + + "\n" + ) + if role == "unsupported" and message[MessageFields.TYPE] == ( + FunctionCatalogControlMessageType.CUSTOM_REGISTRATION_DESTINATION.value + ): + return ControlErrorResponse( + message="Unsupported destination proof fixture" + ).to_dict() + response = super().handle_control_message(message) + if role == "owned" and FunctionCatalogControlField.RESULT.value in response: + result = response[FunctionCatalogControlField.RESULT.value] + if ( + isinstance( + result, CustomFunctionRegistrationControlResponse.value_type + ) + and result.functions + and result.functions[0].name == "registration_live_delayed" + ): + with audit.open("a") as stream: + stream.write( + json.dumps( + { + "event": "persisted_before_delayed_receipt", + "result": str(result.source_file_paths), + "time": time.time(), + } + ) + + "\n" + ) + time.sleep( + 5.5 + ) # Controlled post-mutation fault; production timeout stays5000ms. + return response + + sys.argv = [sys.argv[0], *native_argv] + main(execution_server_type=DiagnosticExecutionServer) + + +def fixture(root: Path) -> tuple[Path, object]: + """Reuse the existing typed synthetic source-projection mechanisms.""" + import numpy as np + from polystore.virtual_workspace import SourcePixelRef + + from openhcs.constants import Microscope + from openhcs.core.image_file_serialization import ImageFileFormat + from openhcs.core.source_projection import ( + OpenHCSPlaneAddress, + SourcePlaneProjection, + SourceProjectionSet, + ) + from openhcs.core.virtual_workspace_metadata import AtomicMetadataWriter + from openhcs.microscopes.source_schema import SourceSchemaFilenameParser + + root.mkdir() + pixels = np.arange(64, dtype=np.uint16).reshape(8, 8) + path = root / "synthetic.tif" + fmt = ImageFileFormat.require_path(path) + from openhcs.core.runtime_image_values import ImagePayloadMetadata + + payload = ImagePayloadMetadata().payload_with(pixels) + fmt.write(path, payload) + projections = SourceProjectionSet( + ( + SourcePlaneProjection( + OpenHCSPlaneAddress.from_values("A01", 1, 1, 1, 1), + SourcePixelRef("disk", path.name), + image_metadata=fmt.persisted_metadata(path, payload), + ), + ) + ) + metadata = projections.metadata_dict( + parser=SourceSchemaFilenameParser(), + microscope_handler_name=Microscope.SOURCE_BINDINGS.value, + source_filename_parser_name="SourceSchemaFilenameParser", + grid_dimensions=[], + pixel_size=1, + main=True, + ) + AtomicMetadataWriter().replace_subdirectory_metadata( + root / "openhcs_metadata.json", ".", metadata + ) + return path, pixels + + +def run(args) -> None: + if Path(sys.executable).resolve() != PYTHON.resolve(): + raise SystemExit("Use the existing shared interpreter.") + if ( + subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=SOURCE, text=True + ).strip() + != args.expected_source_sha + ): + raise SystemExit("Source differs from pinned checkpoint.") + receipt_dir = args.receipts.resolve() + scratch = args.scratch.resolve() + if SOURCE not in receipt_dir.parents or receipt_dir.exists(): + raise SystemExit("Receipts must be a new persistent directory in this tree.") + if scratch.parent != Path("/home/ts/.cache/agent-scratch") or scratch.exists(): + raise SystemExit("Scratch must be a new named owned agent-scratch directory.") + lock = LOCK.open("a+") + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise SystemExit(75) + guard = subprocess.run( + ["/home/ts/bin/agent-resource-check", "--assert-headroom"], + capture_output=True, + text=True, + check=False, + ) + resources = json.loads(guard.stdout) + if resources["ram_available_gib"] < 8 or resources["level"] == "critical": + raise SystemExit("Resource guard disallows this finite validation.") + receipt_dir.mkdir(parents=True) + scratch.mkdir() + owned = scratch / "owned" + foreign = scratch / "foreign" + owned.mkdir() + foreign.mkdir() + for key in ( + "OMP_NUM_THREADS", + "OPENBLAS_NUM_THREADS", + "MKL_NUM_THREADS", + "NUMEXPR_NUM_THREADS", + "NUMBA_NUM_THREADS", + "BLIS_NUM_THREADS", + ): + os.environ[key] = "1" + os.environ.update( + OPENHCS_CPU_ONLY="true", + OPENHCS_HEADLESS="true", + OPENHCS_USE_THREADING="true", + OPENHCS_SUBPROCESS_NO_GPU="1", + CUDA_VISIBLE_DEVICES="", + QT_QPA_PLATFORM="offscreen", + PYTHONDONTWRITEBYTECODE="1", + POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD="false", + POLYSTORE_IMAGEJ_CACHE_ROOT="/home/ts/.cache/polystore/imagej", + OPENHCS_AGENT_READ_ROOTS=os.pathsep.join( + (str(scratch), str(SOURCE / "packaging")) + ), + OPENHCS_AGENT_WRITE_ROOTS=str(owned), + XDG_DATA_HOME=str(owned / "data"), + XDG_CACHE_HOME=str(owned / "cache"), + XDG_CONFIG_HOME=str(owned / "config"), + XDG_STATE_HOME=str(owned / "state"), + XDG_RUNTIME_DIR=str(owned / "runtime"), + NUMBA_CACHE_DIR=str(owned / "numba"), + MPLCONFIGDIR=str(owned / "matplotlib"), + OPENHCS_UI_CONFIG_CACHE_FILE=str(owned / "ui_config.config"), + ) + from dataclasses import replace + + import numpy as np + import polystore + from python_introspect import dataclass_from_mapping + from zmqruntime.client import EndpointShutdownMode + from zmqruntime.config import TransportMode + from zmqruntime.transport import TransportEndpoint, wait_for_endpoint_ready + + import openhcs + from openhcs.mcp.dev_client import McpDevClient + from openhcs.mcp.dev_client_core import McpDevServerSpec + from openhcs.processing.custom_functions.manager import CustomFunctionManager + from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG + from openhcs.runtime.zmq_execution_client import ZMQExecutionClient + from openhcs.serialization.json import to_jsonable + + imports = {"openhcs": openhcs.__file__, "polystore": polystore.__file__} + assert Path(openhcs.__file__).resolve() == SOURCE / "openhcs/__init__.py" + assert ( + Path(polystore.__file__).resolve().is_relative_to(SOURCE / "external/PolyStore") + ) + dependencies = subprocess.check_output( + ["git", "submodule", "status"], cwd=SOURCE, text=True + ).splitlines() + assert not any(row.startswith(("-", "+", "U")) for row in dependencies) + receipt = { + "accepted": False, + "source_sha": args.expected_source_sha, + "imports": imports, + "submodules": dependencies, + "resources": resources, + "driver_pid": os.getpid(), + "source_live_not_installed": True, + "calls": [], + "processes": [], + "original033": "preserved; not replayed", + "shared7777": "never contacted", + "uncertain_dispatch": None, + "scratch": str(scratch), + } + + def save(): + (receipt_dir / "journey.json").write_text(json.dumps(receipt, indent=2) + "\n") + + save() + config = replace( + OPENHCS_ZMQ_CONFIG, transport_mode=TransportMode.TCP, server_host="127.0.0.1" + ) + ports = {"owned": args.port, "foreign": args.port + 1, "unsupported": args.port + 2} + assert 7777 not in ports.values() + endpoints = { + role: TransportEndpoint("127.0.0.1", port, TransportMode.TCP) + for role, port in ports.items() + } + assert not any(endpoint.is_in_use(config) for endpoint in endpoints.values()) + from openhcs.pyqt_gui.config import UIConfig, save_ui_config_sync + + assert save_ui_config_sync( + UIConfig(zmq=replace(config, default_port=args.port, client_host="127.0.0.1")) + ) + image_path, input_pixels = fixture(owned / "plate") + before_hash = hashlib.sha256(image_path.read_bytes()).hexdigest() + probe_code = PROBE.read_text() + source_hash = hashlib.sha256(probe_code.encode()).hexdigest() + store = CustomFunctionManager.default_storage_directory() + from zmqruntime.messages import ExecutionStatus + + from openhcs.agent.dto.execution import ( + ExecutionJobRef, + ExecutionJobStatus, + OrchestratorSessionRef, + ) + from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationRequest, + CustomFunctionRegistrationResult, + FunctionCatalogPage, + FunctionDetail, + ) + + servers = [] + handles = [] + client = None + started = time.monotonic() + + class SourceMcpServerSpec(McpDevServerSpec): + # Explicit source-test projection through the existing launch owner. + mcp_environment_keys = (*McpDevServerSpec.mcp_environment_keys, "PYTHONPATH") + + def call(name, arguments, result_type=None, *, error_code=None): + import psutil + + available = psutil.virtual_memory().available / (1024**3) + receipt.setdefault("available_ram_gib", []).append(available) + if available < 8: + raise RuntimeError("RAM floor reached; no further dispatch.") + if time.monotonic() - started > 240: + raise RuntimeError("Finite journey budget exhausted; no further dispatch.") + row = { + "tool": name, + "arguments": arguments, + "started_utc": time.time(), + "status": "dispatched", + } + receipt["calls"].append(row) + receipt["uncertain_dispatch"] = len(receipt["calls"]) + save() + tick = time.monotonic() + executed = client.execute( + [ + "--allow-error-payloads", + "call", + name, + "--arguments", + json.dumps(arguments), + "--json", + ], + timeout_seconds=10, + ) + row.update( + response=executed.payload, + elapsed_seconds=time.monotonic() - tick, + status="returned", + ) + save() + if executed.payload["errors"]: + raise RuntimeError( + f"Uncertain transport outcome for {name}; do not replay." + ) + payload = executed.payload["results"][0]["payloads"][0] + errors = payload.get("errors", []) + if error_code: + assert errors and errors[0]["code"] == error_code, payload + else: + assert not errors and not executed.payload["results"][0]["mcp_error"], ( + payload + ) + receipt["uncertain_dispatch"] = None + save() + print(f"{name}: {row['elapsed_seconds']:.3f}s", flush=True) + return dataclass_from_mapping(result_type, payload) if result_type else payload + + def register( + name="registration_live_probe", + root=store, + port=args.port, + code=probe_code, + *, + error_code=None, + ): + return call( + "openhcs_register_custom_function", + { + "source_code": code, + "function_name": name, + "storage_dir": str(root), + "port": port, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": True, + }, + None if error_code else CustomFunctionRegistrationResult, + error_code=error_code, + ) + + def finish_job(ref): + deadline = time.monotonic() + 45 + while time.monotonic() < deadline: + status = call( + "openhcs_get_execution_status", + {"job_id": ref.job_id, "timeout_ms": 1000}, + ExecutionJobStatus, + ) + if status.is_terminal: + assert status.status == ExecutionStatus.COMPLETE.value, status + return status + time.sleep( + 0.2 + ) # Existing submitted-job lifecycle observation, never a submit replay. + raise RuntimeError( + "Job observation bound exhausted; retain handles and original job." + ) + + try: + for role, port in ports.items(): + env = os.environ.copy() + if role == "foreign": + env.update( + XDG_DATA_HOME=str(foreign / "data"), + OPENHCS_AGENT_WRITE_ROOTS=str(foreign), + ) + audit = receipt_dir / f"{role}-control.jsonl" + log = (receipt_dir / f"{role}-stderr.txt").open("w") + handles.append(log) + process = subprocess.Popen( + [ + str(PYTHON), + "-B", + str(Path(__file__).resolve()), + "--child-role", + role, + "--child-audit", + str(audit), + "--port", + str(port), + "--host", + "127.0.0.1", + "--transport-mode", + "tcp", + "--persistent", + "--log-file-path", + str(receipt_dir / f"{role}.log"), + ], + env=env, + cwd=SOURCE, + stdout=log, + stderr=subprocess.STDOUT, + ) + servers.append((role, process)) + pong = wait_for_endpoint_ready( + port, TransportMode.TCP, host="127.0.0.1", config=config, timeout=10 + ) + assert pong is not None and pong.process_identity is not None + assert pong.process_identity.pid == process.pid + receipt["processes"].append( + { + "role": role, + "port": port, + "identity": to_jsonable(pong.process_identity), + } + ) + save() + stderr = (receipt_dir / "mcp-stderr.txt").open("w") + handles.append(stderr) + client = McpDevClient( + str(PYTHON), + use_resident_server=False, + server_stderr=stderr, + initialize_timeout_seconds=10, + ) + client.server_spec = SourceMcpServerSpec(str(PYTHON)) + client.start() + health = call("openhcs_health_check", {}) + assert ( + Path(health["server_source_path"]).resolve() + == SOURCE / "openhcs/mcp/server.py" + ) + assert ( + not health["restart_required"] + and not health["server_source_changed_since_import"] + ) + receipt["mcp_pid"] = health["server_process_id"] + receipt["mcp_launch_environment"] = client.server_spec.environment() + call("openhcs_get_authoring_context", {"kind": "first_use"}) + call("openhcs_get_authoring_context", {"kind": "custom_function"}) + call("openhcs_search_capabilities", {"query": "custom function", "limit": 5}) + call( + "openhcs_register_custom_function", + {"source_code": probe_code}, + error_code="mcp_tool_failed", + ) + sentinel = foreign / "registration_live_probe.py" + sentinel.write_bytes(b"preserved outside sentinel\n") + sentinel_hash = hashlib.sha256(sentinel.read_bytes()).hexdigest() + register(root=foreign, error_code="agent_path_policy_rejected") + ancestor = owned / "ancestor-escape" + ancestor.symlink_to(foreign, target_is_directory=True) + register(root=ancestor, error_code="agent_path_policy_rejected") + store.mkdir(parents=True, exist_ok=True) + destination = store / "registration_live_probe.py" + destination.symlink_to(sentinel) + register(error_code="agent_path_policy_rejected") + destination.unlink() # Only the diagnostic symlink, never its target. + register(port=ports["foreign"], error_code="mcp_tool_failed") + register(port=ports["unsupported"], error_code="mcp_tool_failed") + assert hashlib.sha256(sentinel.read_bytes()).hexdigest() == sentinel_hash + for role in ("foreign", "unsupported"): + rows = [ + json.loads(row) + for row in (receipt_dir / f"{role}-control.jsonl") + .read_text() + .splitlines() + ] + assert all( + row.get("type") != CustomFunctionRegistrationRequest.message_type.value + for row in rows + ) + receipt["protected_sentinel"] = { + "path": str(sentinel), + "sha256_before_after": sentinel_hash, + "unchanged": True, + } + registration = register() + assert registration.connection.port == args.port + assert registration.server_identity.pid == servers[0][1].pid + assert registration.source_file_paths == (str(destination),) + assert hashlib.sha256(destination.read_bytes()).hexdigest() == source_hash + page = call( + "openhcs_search_functions", + {"query": "registration_live_probe", "limit": 10}, + FunctionCatalogPage, + ) + assert registration.functions[0].function_id in { + entry.function_id for entry in page.items + } + detail = call( + "openhcs_describe_function", + {"function_id": registration.functions[0].function_id}, + FunctionDetail, + ) + assert ( + detail.entry.import_path + == "openhcs.processing.custom_functions.registration_live_probe" + ) + from openhcs.core.config import ( + LazyStepMaterializationConfig, + MaterializationBackend, + PathPlanningConfig, + PipelineConfig, + VFSConfig, + ) + from openhcs.core.pipeline_document import PipelineDocumentAuthority + from openhcs.core.steps.function_step import FunctionStep + from openhcs.processing.custom_functions import registration_live_probe + + document = PipelineDocumentAuthority.from_values( + pipeline_config=PipelineConfig( + num_workers=1, + use_threading=True, + path_planning_config=PathPlanningConfig( + global_output_folder=owned / "outputs" + ), + vfs_config=VFSConfig( + materialization_backend=MaterializationBackend.DISK + ), + ), + pipeline_steps=[ + FunctionStep( + func=(registration_live_probe, {"offset": 3}), + name="SyntheticPlusThree", + step_materialization_config=LazyStepMaterializationConfig( + enabled=True + ), + ) + ], + ) + pipeline_source = PipelineDocumentAuthority.render(document) + (receipt_dir / "pipeline.py").write_text(pipeline_source) + inspected = call( + "openhcs_inspect_pipeline_source_artifact_plan", + { + "plate_path": str(image_path.parent), + "pipeline_source": pipeline_source, + }, + ) + session = call( + "openhcs_create_orchestrator_session_from_pipeline_source", + { + "plate_path": str(image_path.parent), + "pipeline_source": pipeline_source, + "port": args.port, + "host": "127.0.0.1", + "transport_mode": "tcp", + }, + OrchestratorSessionRef, + ) + compiled = finish_job( + call( + "openhcs_submit_compile", + {"session_id": session.session_id, "wait": False}, + ExecutionJobRef, + ) + ) + receipt["compile_response"] = to_jsonable(compiled) + executed = finish_job( + call( + "openhcs_submit_pipeline_execution", + {"session_id": session.session_id, "wait": False}, + ExecutionJobRef, + ) + ) + receipt["execution_response"] = to_jsonable(executed) + output_images = list((owned / "outputs").rglob("*.tif")) + assert output_images, inspected + from openhcs.core.image_file_serialization import ImageFileFormat + + checked = [] + for output in output_images: + image = ImageFileFormat.require_path(output).read(output) + actual = np.asarray(image) + np.testing.assert_array_equal(actual, input_pixels + 3) + checked.append( + { + "path": str(output), + "sha256": hashlib.sha256(output.read_bytes()).hexdigest(), + "shape": list(actual.shape), + } + ) + receipt["checked_outputs"] = checked + assert hashlib.sha256(image_path.read_bytes()).hexdigest() == before_hash + delayed_code = probe_code.replace( + "registration_live_probe", "registration_live_delayed" + ) + register( + name="registration_live_delayed", + code=delayed_code, + error_code="custom_function_registration_uncertain", + ) + delayed_path = store / "registration_live_delayed.py" + assert delayed_path.read_text() == delayed_code + delayed = call( + "openhcs_search_functions", + {"query": "registration_live_delayed", "limit": 5}, + FunctionCatalogPage, + ) + assert ( + len( + [ + entry + for entry in delayed.items + if entry.name == "registration_live_delayed" + ] + ) + == 1 + ) + audit_rows = [ + json.loads(row) + for row in (receipt_dir / "owned-control.jsonl").read_text().splitlines() + ] + assert ( + len( + [ + row + for row in audit_rows + if row.get("event") == "persisted_before_delayed_receipt" + ] + ) + == 1 + ) + receipt.update( + accepted=True, + no_registration_replay=True, + unchanged_input_sha256=before_hash, + ) + save() + except BaseException as error: + receipt["failure"] = { + "type": type(error).__name__, + "message": str(error), + "traceback": traceback.format_exc(), + } + save() + print( + "STOPPED: original calls/handles preserved. Inspect on same handle or enter close-owned; never replay.", + flush=True, + ) + for command in sys.stdin: + if command.strip() == "close-owned": + receipt["operator_disposition"] = ( + "Close only diagnostic owned processes; do not replay any recorded call." + ) + receipt["uncertain_dispatch"] = None + save() + break + if client is not None: + import shlex + + observation = client.execute(shlex.split(command), timeout_seconds=10) + receipt.setdefault("same_handle_observations", []).append( + observation.payload + ) + save() + print(observation.rendered_output, flush=True) + else: + raise RuntimeError( + "No operator disposition; retain diagnostic handles." + ) from error + finally: + if receipt["uncertain_dispatch"] is None: + if client is not None: + client.close() + receipt["client_closed"] = True + for role, process in reversed(servers): + process_row = next( + (row for row in receipt["processes"] if row["role"] == role), + None, + ) + if process.poll() is not None: + receipt.setdefault("closed_servers", []).append( + {"role": role, "returncode": process.returncode} + ) + continue + if process_row is None: + # Popen retains this exact child handle even if startup failed. + process.terminate() + process.wait(timeout=10) + receipt.setdefault("closed_servers", []).append( + { + "role": role, + "returncode": process.returncode, + "startup_failed": True, + } + ) + continue + identity = process_row["identity"] + heartbeat = endpoints[role].ping(config, timeout_ms=1000) + assert ( + heartbeat is not None + and to_jsonable(heartbeat.process_identity) == identity + ) + shutdown = ZMQExecutionClient.shutdown_endpoint_on_port( + ports[role], + EndpointShutdownMode.FORCE, + timeout=5, + transport_mode=TransportMode.TCP, + host="127.0.0.1", + config=config, + ) + process.wait(timeout=10) + receipt.setdefault("closed_servers", []).append( + { + "role": role, + "shutdown": to_jsonable(shutdown), + "returncode": process.returncode, + } + ) + for handle in handles: + handle.close() + receipt["runtime_terminal"] = True + save() + fcntl.flock(lock, fcntl.LOCK_UN) + lock.close() + save() + print( + json.dumps( + { + "receipt": str(receipt_dir / "journey.json"), + "accepted": receipt["accepted"], + "terminal": receipt.get("runtime_terminal"), + } + ), + flush=True, + ) + + +def main(): + if "--child-role" in sys.argv: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( + "--child-role", choices=("owned", "foreign", "unsupported"), required=True + ) + parser.add_argument("--child-audit", type=Path, required=True) + args, remainder = parser.parse_known_args() + child_server(args.child_role, args.child_audit, remainder) + return + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--released-runtime-slot", action="store_true", required=True) + parser.add_argument("--expected-source-sha", required=True) + parser.add_argument("--receipts", type=Path, required=True) + parser.add_argument("--scratch", type=Path, required=True) + parser.add_argument("--port", type=int, default=15991) + run(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/tests/diagnostics/registration_probe_source.py b/tests/diagnostics/registration_probe_source.py new file mode 100644 index 0000000000..98b95ddadd --- /dev/null +++ b/tests/diagnostics/registration_probe_source.py @@ -0,0 +1,8 @@ +"""Synthetic identity-plus-three contract; no file IO or biological semantics.""" + +from openhcs.core.memory import numpy + + +@numpy +def registration_live_probe(image, offset=3): + return image + offset diff --git a/tests/unit/agent/test_custom_registration_admission.py b/tests/unit/agent/test_custom_registration_admission.py index 3bfd16a6d1..6fecee2f7f 100644 --- a/tests/unit/agent/test_custom_registration_admission.py +++ b/tests/unit/agent/test_custom_registration_admission.py @@ -371,6 +371,42 @@ def test_manager_admitted_persistence_uses_existing_registry_and_source_owner( CustomFunctionRuntimeRegistry.clear() +@pytest.mark.parametrize("operation", ("require", "load", "read", "delete", "update")) +def test_manager_named_source_operations_use_filename_owner( + tmp_path, monkeypatch, operation +): + from openhcs.processing.custom_functions.source_namespace import ( + CustomFunctionSource, + ) + + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + manager = CustomFunctionManager(create_storage=False) + selected = [] + + def own_path(root, name): + selected.append((root, name)) + raise LookupError("filename-owner-witness") + + monkeypatch.setattr(manager, "source_path_for_name", own_path) + operations = { + "require": lambda: manager.require_source( + CustomFunctionSource( + function_name="boundary_probe", content_sha256="0" * 64 + ) + ), + "load": lambda: manager.load_custom_function("boundary_probe"), + "read": lambda: manager.get_function_code("boundary_probe"), + "delete": lambda: manager.delete_custom_function("boundary_probe"), + "update": lambda: manager.update_custom_function( + "boundary_probe", "not evaluated" + ), + } + with pytest.raises(LookupError, match="filename-owner-witness"): + operations[operation]() + assert selected == [(manager.storage_dir, "boundary_probe")] + assert not manager.storage_dir.exists() + + @pytest.mark.parametrize("missing_route", (False, True)) def test_real_generated_mcp_boundary_excludes_authority_and_denies_before_client( tmp_path, missing_route From c10bb3f858d88e7d8fff2fbbfee61c40ca81c887 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:59:04 -0400 Subject: [PATCH 06/13] Retain strict non-swap resource gate and pre-live acceptance boundary --- .../custom_registration_admission_20260929.md | 14 ++++++++++++++ .../diagnostics/check_custom_registration_live.py | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 454222fdec..ca4ba02572 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -193,6 +193,20 @@ one native thread; acquires the shared nonblocking lock and records measured call durations (not session ages), sentinels, original inputs and handles. Unexpected calls are not retried. This prepared driver is not a passing receipt. +Pre-live checkpoint9020cae18 is published in the existing draft233. New source +changes were completed before any live handle existed. Ruff on both diagnostic +files and the admission test, `git diff --check`, and the stdlib-only diagnostic +CLI help pass. Five new filename-owner cases are written but have **not yet +run**; the earlier94/23 results do not cover these changes or this main merge. + +The actual guard now warns about non-swap disk headroom (/home19.8 GiB), with +RAM13.0–13.6 GiB. Parent explicitly withdrew new heavy-runtime admission until +its own cleanup and recheck. No live process, JVM, GUI, registration call or +validation-lock holder was started here. The diagnostic rejects any non-swap +guard warning; source-only preparation/publication continues. Remaining named +dependency is parent's verified resource-headroom release, followed by this +source-pinned synthetic journey and parent-owned installation/acceptance. + After verified closure of the frozen author's owned MCP/viewer and release of the technical slot, run controlled owned-vs-shared endpoint, escaping-path and delayed-response checks, plus register/discover/compile/execute on a tiny diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 1e3a427a01..33b8c93077 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -161,7 +161,9 @@ def run(args) -> None: check=False, ) resources = json.loads(guard.stdout) - if resources["ram_available_gib"] < 8 or resources["level"] == "critical": + if resources["ram_available_gib"] < 8 or any( + not reason.startswith("swap used ") for reason in resources["reasons"] + ): raise SystemExit("Resource guard disallows this finite validation.") receipt_dir.mkdir(parents=True) scratch.mkdir() From 81e636ad43566deb4a46392b9474dad40da16853 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 19:06:32 -0400 Subject: [PATCH 07/13] Record five filename owner regressions and bound live scratch admission --- .../custom_registration_admission_20260929.md | 18 ++++++++- .../check_custom_registration_live.py | 27 ++++++++++--- .../pytest.txt | 14 +++++++ .../receipt.json | 39 +++++++++++++++++++ 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 tests/runtime_diagnostics/registration_owner_five_20260929/pytest.txt create mode 100644 tests/runtime_diagnostics/registration_owner_five_20260929/receipt.json diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index ca4ba02572..2eaa3255e4 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -196,8 +196,14 @@ Unexpected calls are not retried. This prepared driver is not a passing receipt. Pre-live checkpoint9020cae18 is published in the existing draft233. New source changes were completed before any live handle existed. Ruff on both diagnostic files and the admission test, `git diff --check`, and the stdlib-only diagnostic -CLI help pass. Five new filename-owner cases are written but have **not yet -run**; the earlier94/23 results do not cover these changes or this main merge. +CLI help pass. At exact sourcec10bb3f85, the five new filename-owner cases +**pass in3.02 seconds**, peak183520 KiB RSS, with OpenHCS and PolyStore imports +verified inside this tree. They intercept `require_source`, named load/read, +delete and update before source reads/evaluation, proving use of the filename +owner. The earlier94/23 results do not cover this main merge. Exact command, +paths and output: `tests/runtime_diagnostics/registration_owner_five_20260929/` +(`receipt.json`, `pytest.txt`). Two pytest warnings concern disabled async +plugins' configuration options; these five cases are synchronous. The actual guard now warns about non-swap disk headroom (/home19.8 GiB), with RAM13.0–13.6 GiB. Parent explicitly withdrew new heavy-runtime admission until @@ -207,6 +213,14 @@ guard warning; source-only preparation/publication continues. Remaining named dependency is parent's verified resource-headroom release, followed by this source-pinned synthetic journey and parent-owned installation/acceptance. +Parent explicitly authorized just these five bounded lightweight cases during +its cleanup. The single existing-interpreter test process used the nonblocking +validation lock, one native thread and no conftest/auto-loaded plugins. It is +terminal and the lock released. Guard at this test's dispatch showed RAM13.4 +GiB, /home20.1 GiB, and only historical-swap warning; this does **not** assume +parent's heavy-runtime slot release. Owned disposable source-test scratch: +`/home/ts/.cache/agent-scratch/openhcs-registration-owner-five-20260929`. + After verified closure of the frozen author's owned MCP/viewer and release of the technical slot, run controlled owned-vs-shared endpoint, escaping-path and delayed-response checks, plus register/discover/compile/execute on a tiny diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 33b8c93077..029b025fe2 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -293,12 +293,27 @@ class SourceMcpServerSpec(McpDevServerSpec): mcp_environment_keys = (*McpDevServerSpec.mcp_environment_keys, "PYTHONPATH") def call(name, arguments, result_type=None, *, error_code=None): - import psutil - - available = psutil.virtual_memory().available / (1024**3) - receipt.setdefault("available_ram_gib", []).append(available) - if available < 8: - raise RuntimeError("RAM floor reached; no further dispatch.") + check = subprocess.run( + ["/home/ts/bin/agent-resource-check", "--assert-headroom"], + capture_output=True, + text=True, + check=False, + ) + current_resources = json.loads(check.stdout) + scratch_bytes = sum( + path.lstat().st_size for path in scratch.rglob("*") if path.is_file() + ) + receipt.setdefault("resource_observations", []).append( + {"guard": current_resources, "scratch_bytes": scratch_bytes} + ) + save() + if scratch_bytes >= 80 * 1024 * 1024: + raise RuntimeError("80MiB scratch bound reached; no further dispatch.") + if current_resources["ram_available_gib"] < 8 or any( + not reason.startswith("swap used ") + for reason in current_resources["reasons"] + ): + raise RuntimeError("Non-swap resource gate closed; no further dispatch.") if time.monotonic() - started > 240: raise RuntimeError("Finite journey budget exhausted; no further dispatch.") row = { diff --git a/tests/runtime_diagnostics/registration_owner_five_20260929/pytest.txt b/tests/runtime_diagnostics/registration_owner_five_20260929/pytest.txt new file mode 100644 index 0000000000..d04d1836c5 --- /dev/null +++ b/tests/runtime_diagnostics/registration_owner_five_20260929/pytest.txt @@ -0,0 +1,14 @@ +..... [100%] +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +5 passed, 2 warnings in 3.02s diff --git a/tests/runtime_diagnostics/registration_owner_five_20260929/receipt.json b/tests/runtime_diagnostics/registration_owner_five_20260929/receipt.json new file mode 100644 index 0000000000..b6626c9422 --- /dev/null +++ b/tests/runtime_diagnostics/registration_owner_five_20260929/receipt.json @@ -0,0 +1,39 @@ +{ + "source_sha": "c10bb3f858d88e7d8fff2fbbfee61c40ca81c887", + "imports": { + "openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", + "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py" + }, + "arguments": [ + "--noconftest", + "--import-mode=importlib", + "-p", + "no:cacheprovider", + "-o", + "addopts=", + "-q", + "--tb=short", + "--basetemp=/home/ts/.cache/agent-scratch/openhcs-registration-owner-five-20260929/pytest", + "tests/unit/agent/test_custom_registration_admission.py::test_manager_named_source_operations_use_filename_owner" + ], + "exit_code": 0, + "elapsed_seconds": 3.0643175909935962, + "peak_rss_kib": 183520, + "headroom_before": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.1, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 13.4, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scope": "five owner-interception source cases only; parent-authorized lightweight check despite disk warning", + "no_mcp_gui_jvm_install_or_science": true, + "owned_disposable_scratch": "/home/ts/.cache/agent-scratch/openhcs-registration-owner-five-20260929" +} From 908b4b03e2c98cb2c0cd5ad64af086067a434433 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 19:17:34 -0400 Subject: [PATCH 08/13] Remove registration resend loop and bound preparation by native deadline --- .../endpoint_function_catalog_service.py | 17 ++++- openhcs/runtime/zmq_execution_client.py | 39 ++++++---- .../references/custom-function-authoring.md | 7 ++ .../check_custom_registration_live.py | 34 ++++++++- .../test_custom_registration_admission.py | 46 ++++++++++++ tests/unit/test_function_catalog_zmq.py | 72 +++++++++++++++++-- 6 files changed, 190 insertions(+), 25 deletions(-) diff --git a/openhcs/agent/services/endpoint_function_catalog_service.py b/openhcs/agent/services/endpoint_function_catalog_service.py index 7db9fc626f..fc1c5886f0 100644 --- a/openhcs/agent/services/endpoint_function_catalog_service.py +++ b/openhcs/agent/services/endpoint_function_catalog_service.py @@ -10,7 +10,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING -from zmqruntime import OperationCancellation +from zmqruntime import OperationCancellation, OperationDeadline from openhcs.agent.dto.functions import ( DEFAULT_FUNCTION_DETAIL_DOC_CHARS, @@ -376,9 +376,13 @@ def register_custom_function( transport_mode=request.connection.transport_endpoint().transport_mode, persistent=request.connection.persistent, ) + deadline = OperationDeadline.after_milliseconds( + endpoint.control_timeout_ms, operation="custom function registration", + ) client = self._client_for(endpoint) destination = client.custom_function_registration_destination( - CustomFunctionRegistrationDestinationRequest(function_name=request.function_name) + CustomFunctionRegistrationDestinationRequest(function_name=request.function_name), + operation_deadline=deadline, ) destination.require_request(request) if request.persist: @@ -387,8 +391,15 @@ def register_custom_function( request = replace(request, server_identity=destination.server_identity) self.invalidate() self._config_provider = lambda: endpoint + # The existing read-only owner may poll preparation, but it must not + # carry executable source. Failure here precedes mutation dispatch. + client.get_function_catalog( + FunctionCatalogControlRequest(compact_signatures=request.compact_signatures), + operation_deadline=deadline, + ) + deadline.remaining_seconds() try: - result = client.register_custom_function(request) + result = client.register_custom_function(request, operation_deadline=deadline) destination.require_result(request, result) if not request.persist: from openhcs.processing.custom_functions.manager import ( diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 8aef01e8c4..9abeb0ebe1 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -814,6 +814,7 @@ def get_function_catalog( request: FunctionCatalogControlRequest, *, cancellation: OperationCancellation | None = None, + operation_deadline: OperationDeadline | None = None, ) -> FunctionCatalogPage: """Read the authoritative callable catalog from this execution endpoint.""" @@ -828,12 +829,14 @@ def get_function_catalog( ) if not connection_attempt.connect( EndpointConnectionPolicy.ATTACH_OR_START, - self.config.client_connect_timeout_seconds, + (self.config.client_connect_timeout_seconds if operation_deadline is None + else operation_deadline.cap_seconds(self.config.client_connect_timeout_seconds)), ): raise RuntimeError("Failed to connect to execution server") response = self._send_function_catalog_control_request( FunctionCatalogControlPayload.from_request(request).to_dict(), cancellation=cancellation, + operation_deadline=operation_deadline, ) return FunctionCatalogControlResponse.from_control_response(response).catalog @@ -896,25 +899,31 @@ def get_function_reference( def register_custom_function( self, request: CustomFunctionRegistrationRequest, + *, operation_deadline: OperationDeadline | None = None, ) -> CustomFunctionRegistrationResult: - """Register custom source through this execution endpoint's catalog.""" + """Send one mutation; readiness belongs to preceding read-only discovery. + + Never poll/resend a source-bearing request, including when the server + reports preparation pending. A missing mutation receipt is uncertain. + """ from openhcs.agent.dto.functions import ( CustomFunctionRegistrationControlResponse, FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect(): + if not self.is_connected() and not self.connect(operation_deadline=operation_deadline): raise RuntimeError("Failed to connect to execution server") - response = self._send_function_catalog_control_request( - FunctionCatalogControlPayload.from_request(request).to_dict() - ) + payload = FunctionCatalogControlPayload.from_request(request).to_dict() + response = (self._send_control_request(payload) if operation_deadline is None + else self._send_control_request(payload, timeout_ms=operation_deadline.remaining_milliseconds())) return CustomFunctionRegistrationControlResponse.from_control_response( response ).result def custom_function_registration_destination( self, request: CustomFunctionRegistrationDestinationRequest, + *, operation_deadline: OperationDeadline | None = None, ) -> CustomFunctionRegistrationDestination: """Require the selected endpoint's native admission contract before mutation.""" from openhcs.agent.dto.functions import ( @@ -922,11 +931,11 @@ def custom_function_registration_destination( FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect(): + if not self.is_connected() and not self.connect(operation_deadline=operation_deadline): raise RuntimeError("Failed to connect to execution server") - response = self._send_control_request( - FunctionCatalogControlPayload.from_request(request).to_dict() - ) + payload = FunctionCatalogControlPayload.from_request(request).to_dict() + response = (self._send_control_request(payload) if operation_deadline is None + else self._send_control_request(payload, timeout_ms=operation_deadline.remaining_milliseconds())) return CustomFunctionRegistrationDestinationControlResponse.from_control_response(response).destination def _send_function_catalog_control_request( @@ -934,8 +943,9 @@ def _send_function_catalog_control_request( request: dict, *, cancellation: OperationCancellation | None = None, + operation_deadline: OperationDeadline | None = None, ) -> dict: - """Poll a responsive endpoint while its catalog preparation is active.""" + """Poll read-only discovery while endpoint catalog preparation is active.""" from openhcs.agent.dto.functions import ( FunctionCatalogPreparationControlResponse, @@ -946,7 +956,8 @@ def _send_function_catalog_control_request( while True: if cancellation.requested(): raise CancelledError("Function catalog preparation was cancelled") - response = self._send_control_request(request) + response = (self._send_control_request(request) if operation_deadline is None + else self._send_control_request(request, timeout_ms=operation_deadline.remaining_milliseconds())) pending = FunctionCatalogPreparationControlResponse.from_control_response( response ) @@ -963,7 +974,9 @@ def _send_function_catalog_control_request( pending.status.phase, pending.status.message, ) - if cancellation.wait(pending.retry_after_seconds): + wait_seconds = (pending.retry_after_seconds if operation_deadline is None + else operation_deadline.cap_seconds(pending.retry_after_seconds)) + if cancellation.wait(wait_seconds): raise CancelledError("Function catalog preparation was cancelled") def send_debug_worker_command( diff --git a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md index 7593fdf922..2c7ba6d581 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md +++ b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md @@ -96,6 +96,13 @@ evaluating the source. The execution service independently admits the native write before evaluation and persistence. Receipt checks after writing verify the outcome; they are not write admission or a Python sandbox. +Complete read-only catalog discovery on the intended endpoint before mutation. +Cold preparation failures belong to that boundary: the service prepares via +the existing read-only catalog request, then sends registration exactly once. +It never polls/resends a source-bearing request, even if a server responds +preparation-pending. A read-only observation timeout does not justify a new +mutation; retain its handle/progress and reconcile that preparation first. + For isolated sessions, also pin `OPENHCS_UI_CONFIG_CACHE_FILE` before MCP startup and verify the selected catalog. That launch-time selector still owns initial discovery; passing a later GUI connection does not redirect it. diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 029b025fe2..10c8d01431 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -27,8 +27,10 @@ def child_server(role: str, audit: Path, native_argv: list[str]) -> None: """Diagnostic fault seam on the real launcher/server, not another registry.""" + import polystore from zmqruntime.messages import ControlErrorResponse, MessageFields + import openhcs from openhcs.agent.dto.functions import ( CustomFunctionRegistrationControlResponse, FunctionCatalogControlField, @@ -37,6 +39,16 @@ def child_server(role: str, audit: Path, native_argv: list[str]) -> None: from openhcs.runtime.zmq_execution_server import ZMQExecutionServer from openhcs.runtime.zmq_execution_server_launcher import main + native_imports = {"openhcs": openhcs.__file__, "polystore": polystore.__file__} + assert Path(openhcs.__file__).resolve() == SOURCE / "openhcs/__init__.py" + assert ( + Path(polystore.__file__).resolve().is_relative_to(SOURCE / "external/PolyStore") + ) + with audit.open("a") as stream: + stream.write( + json.dumps({"event": "native_imports", "paths": native_imports}) + "\n" + ) + class DiagnosticExecutionServer(ZMQExecutionServer): _server_type = "registration_acceptance_fixture" @@ -136,6 +148,11 @@ def fixture(root: Path) -> tuple[Path, object]: def run(args) -> None: if Path(sys.executable).resolve() != PYTHON.resolve(): raise SystemExit("Use the existing shared interpreter.") + if args.installed_entrypoint: + if "PYTHONPATH" in os.environ: + raise SystemExit("Installed acceptance requires PYTHONPATH unset.") + if Path.cwd().resolve().is_relative_to(SOURCE): + raise SystemExit("Installed acceptance requires cwd outside source.") if ( subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=SOURCE, text=True @@ -236,7 +253,11 @@ def run(args) -> None: "submodules": dependencies, "resources": resources, "driver_pid": os.getpid(), - "source_live_not_installed": True, + "source_live_not_installed": not args.installed_entrypoint, + "installed_entrypoint": args.installed_entrypoint, + "launch_cwd": str( + Path.cwd().resolve() if args.installed_entrypoint else SOURCE + ), "calls": [], "processes": [], "original033": "preserved; not replayed", @@ -432,7 +453,7 @@ def finish_job(ref): str(receipt_dir / f"{role}.log"), ], env=env, - cwd=SOURCE, + cwd=Path(receipt["launch_cwd"]), stdout=log, stderr=subprocess.STDOUT, ) @@ -458,7 +479,8 @@ def finish_job(ref): server_stderr=stderr, initialize_timeout_seconds=10, ) - client.server_spec = SourceMcpServerSpec(str(PYTHON)) + if not args.installed_entrypoint: + client.server_spec = SourceMcpServerSpec(str(PYTHON)) client.start() health = call("openhcs_health_check", {}) assert ( @@ -510,6 +532,7 @@ def finish_job(ref): "sha256_before_after": sentinel_hash, "unchanged": True, } + call("openhcs_search_functions", {"query": "registration_live_probe", "limit": 5}, FunctionCatalogPage) registration = register() assert registration.connection.port == args.port assert registration.server_identity.pid == servers[0][1].pid @@ -776,6 +799,11 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--released-runtime-slot", action="store_true", required=True) parser.add_argument("--expected-source-sha", required=True) + parser.add_argument( + "--installed-entrypoint", + action="store_true", + help="Require ordinary installed imports with PYTHONPATH unset and cwd outside source.", + ) parser.add_argument("--receipts", type=Path, required=True) parser.add_argument("--scratch", type=Path, required=True) parser.add_argument("--port", type=int, default=15991) diff --git a/tests/unit/agent/test_custom_registration_admission.py b/tests/unit/agent/test_custom_registration_admission.py index 6fecee2f7f..7c5b9966b3 100644 --- a/tests/unit/agent/test_custom_registration_admission.py +++ b/tests/unit/agent/test_custom_registration_admission.py @@ -117,6 +117,10 @@ def register_custom_function(self, admitted): source_file_paths=(str(root / "boundary_probe.py"),), ) + def get_function_catalog(self, read_request): + assert not hasattr(read_request, "source_code") + assert not mutations + def disconnect(self): pass @@ -277,6 +281,10 @@ def register_custom_function(self, admitted): server_identity=replace(ProcessIdentity.current(), create_time=0), ) + def get_function_catalog(self, read_request): + assert not hasattr(read_request, "source_code") + assert not mutations + def disconnect(self): pass @@ -296,6 +304,44 @@ def disconnect(self): catalog.close() +def test_readiness_failure_precedes_mutation_and_is_not_postdispatch_uncertainty( + tmp_path, +): + observed = [] + + class Client: + def custom_function_registration_destination(self, probe): + observed.append("destination") + return CustomFunctionRegistrationDestination( + str(tmp_path), str(tmp_path / "boundary_probe.py") + ) + + def get_function_catalog(self, read_request): + assert not hasattr(read_request, "source_code") + observed.append("read-only-preparation") + raise RuntimeError("native preparation failed") + + def register_custom_function(self, admitted): + pytest.fail("Source must not be dispatched after readiness failure") + + def disconnect(self): + pass + + catalog = ZMQFunctionCatalogService( + lambda: OPENHCS_ZMQ_CONFIG, + client_factory=lambda endpoint: Client(), + path_policy=policy(tmp_path), + ) + try: + with pytest.raises(RuntimeError, match="native preparation failed") as failure: + catalog.register_custom_function(request(tmp_path)) + assert not isinstance(failure.value, CustomFunctionRegistrationUncertainError) + assert observed == ["destination", "read-only-preparation"] + assert catalog._config_provider().default_port == 15993 + finally: + catalog.close() + + def test_registration_cli_projects_declarations_and_rejects_missing_route(tmp_path): from openhcs.mcp.dev_client import _build_parser, _calls_from_args from openhcs.mcp.dev_client_core import McpDevCliUsageError diff --git a/tests/unit/test_function_catalog_zmq.py b/tests/unit/test_function_catalog_zmq.py index b8f8e67361..2388d9a4c8 100644 --- a/tests/unit/test_function_catalog_zmq.py +++ b/tests/unit/test_function_catalog_zmq.py @@ -360,6 +360,52 @@ def test_catalog_client_cancellation_prevents_another_poll(monkeypatch) -> None: ) +@pytest.mark.parametrize("response_kind", ("ready", "pending", "timeout")) +def test_registration_client_sends_mutation_once_even_if_pending_or_uncertain( + monkeypatch, + response_kind, +) -> None: + client = ZMQExecutionClient(port=22319, persistent=True) + monkeypatch.setattr(client, "is_connected", lambda: True) + observed = [] + result = CustomFunctionRegistrationResult(schema_version="openhcs.agent.v1") + + def send(payload): + observed.append(payload) + if response_kind == "timeout": + raise TimeoutError("postdispatch observation") + if response_kind == "pending": + return FunctionCatalogPreparationControlResponse( + status=EndpointStartupStatus( + phase=EndpointStartupPhase.PREPARING_CAPABILITIES, + message="Preparing", + sequence=1, + timestamp=1.0, + ), + ).to_control_response() + return CustomFunctionRegistrationControlResponse( + value=result + ).to_control_response() + + monkeypatch.setattr(client, "_send_control_request", send) + monkeypatch.setattr( + client, + "_send_function_catalog_control_request", + lambda _payload: pytest.fail("Mutation cannot use the preparation poll loop"), + ) + request = CustomFunctionRegistrationRequest( + source_code="not evaluated", persist=False + ) + if response_kind == "ready": + assert client.register_custom_function(request) is result + else: + with pytest.raises( + TimeoutError if response_kind == "timeout" else RuntimeError + ): + client.register_custom_function(request) + assert observed == [FunctionCatalogControlPayload.from_request(request).to_dict()] + + def test_catalog_client_applies_request_cancellation_to_endpoint_startup( monkeypatch, ) -> None: @@ -533,23 +579,37 @@ def test_zmq_router_registers_custom_source_through_catalog_owner( def test_native_registration_destination_bypasses_catalog_preparation( - tmp_path, monkeypatch, + tmp_path, + monkeypatch, ) -> None: from zmqruntime.messages import ProcessIdentity monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "absent")) context = _context(Future()) + def reject_preparation(): - raise AssertionError("Destination admission cannot prepare the function catalog") - monkeypatch.setattr(context.function_catalog_preparation, "ensure_started", reject_preparation) + raise AssertionError( + "Destination admission cannot prepare the function catalog" + ) + + monkeypatch.setattr( + context.function_catalog_preparation, "ensure_started", reject_preparation + ) response = ZMQControlMessageRouter.handle( FunctionCatalogControlPayload.from_request( CustomFunctionRegistrationDestinationRequest(function_name="boundary_probe") - ).to_dict(), context, + ).to_dict(), + context, + ) + destination = ( + CustomFunctionRegistrationDestinationControlResponse.from_control_response( + response + ).destination ) - destination = CustomFunctionRegistrationDestinationControlResponse.from_control_response(response).destination assert destination.server_identity == ProcessIdentity.current() - assert destination.source_file_path == str(tmp_path / "absent" / "openhcs" / "custom_functions" / "boundary_probe.py") + assert destination.source_file_path == str( + tmp_path / "absent" / "openhcs" / "custom_functions" / "boundary_probe.py" + ) assert not (tmp_path / "absent").exists() From 825eec2d1b5eb3eb007ab229ae6a30021cc55cac Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 19:49:05 -0400 Subject: [PATCH 09/13] Expose incarnation-owned catalog preparation and gate single source dispatch --- .../custom_registration_admission_20260929.md | 389 +++----- .../mcp_knowledge_base_manifest.json | 4 +- openhcs/agent/capabilities.py | 56 +- openhcs/agent/dto/functions.py | 88 ++ .../endpoint_function_catalog_service.py | 111 ++- openhcs/mcp/context.py | 43 +- .../runtime/function_catalog_preparation.py | 88 +- openhcs/runtime/zmq_control.py | 131 ++- openhcs/runtime/zmq_execution_client.py | 86 +- .../references/custom-function-authoring.md | 35 +- .../check_custom_registration_live.py | 56 +- .../foreign-control.jsonl | 2 + .../foreign-stderr.txt | 20 + .../journey.json | 921 ++++++++++++++++++ .../mcp-stderr.txt | 1 + .../owned-control.jsonl | 140 +++ .../owned-stderr.txt | 25 + .../unsupported-control.jsonl | 2 + .../unsupported-stderr.txt | 20 + .../build.log | 5 + .../import.json | 9 + .../resources.txt | 23 + .../pytest.txt | 16 + .../receipt.json | 40 + .../resources.txt | 1 + .../test_custom_registration_admission.py | 112 ++- tests/unit/test_function_catalog_zmq.py | 221 ++++- 27 files changed, 2308 insertions(+), 337 deletions(-) create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/journey.json create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/mcp-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_native_build_20260929/build.log create mode 100644 tests/runtime_diagnostics/registration_native_build_20260929/import.json create mode 100644 tests/runtime_diagnostics/registration_native_build_20260929/resources.txt create mode 100644 tests/runtime_diagnostics/registration_readiness_20260929/pytest.txt create mode 100644 tests/runtime_diagnostics/registration_readiness_20260929/receipt.json create mode 100644 tests/runtime_diagnostics/registration_readiness_20260929/resources.txt diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 2eaa3255e4..0a5d67fe1d 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -1,235 +1,154 @@ -# Custom-function admission (#230) - -Implementation owner: Lovelace; integration/live scheduling owner: coordinator. -Source tree: `/home/ts/wt/openhcs-custom-function-admission-20260929`. -Initial base: `openhcsdev/main` 6c2bc4671. Subsequent normal integration of -main250fd1ecf (#234) is merge5899d6323; recorded PolyStore1209068 is checked out -only in this worker's isolated submodule. Implementation commits625e82fce and -4ffc2b202 precede that merge. No frozen installed source is edited. - -## Verified boundary and checkpoint - -The preserved H002 receipt033 timed out, but the shared execution log records -saving `/home/ts/.local/share/openhcs/custom_functions/detect_h002_centres.py` at -22:00:08 UTC. The missing cache selector selected shared7777. Documentation -already requires the selector; additional prose does not enforce admission. -No original request is replayed, no saved side effect is removed, and no assay -source, pixels or output is used in the regression fixtures. - -Registration now requires an explicit `ExecutionConnectionSpec` port. Persisted -registration also requires the intended native store and public function name. -`AgentPathPolicy` admits that store and its exact owner-derived source path -before even creating an execution client. A read-only control request asks the -selected server's existing `CustomFunctionManager` owner for its actual store -without source evaluation, catalog preparation or directory creation. A -different or unsupported server fails before executable source is dispatched. -The destination carries existing `ProcessIdentity` (PID plus creation time). -That exact token is required by the native service before source evaluation and -returned with the typed connection in the mutation receipt. A changed owner or -malformed mutation receipt is not silently accepted. The server repeats both -its own and the caller's admission before evaluation and persistence; caller -roots and the selected token are internal transport state, not public MCP -parameters. The real generated tool signature verifies their exclusion. - -Successful admission selects the existing catalog connection for subsequent -discovery/reference operations. Post-dispatch transport failure is reported as -`custom_function_registration_uncertain`; it does not retry or select a fallback -endpoint. Existing control timeout limits are unchanged. - -## Focused evidence and resource bounds - -Explicit shared-venv Python with this worktree and all eight recorded submodule -`src` paths: OpenHCS, ObjectState, metaclass_registry and PolyStore import paths -were verified in this source tree. Ten provider-free tests pass in 2.41 seconds, -with 181760 KiB peak RSS; available RAM after the check was 14.26 GiB. The resource -guard warned about historical swap; no heavy runtime was started. The tests -cover missing-route denial, ordinary/ancestor/file-symlink denial before client -creation, native-store mismatch before mutation, exact selected route, -single-dispatch uncertainty, read-only destination lookup and public-authority -field exclusion. `git diff --check` and new-test Ruff checks pass. Whole touched -source Ruff is not claimed clean: existing unrelated findings remain. - -Follow-up evidence: **94 tests passed, two plugin-disabled configuration warnings, -10.41 seconds, peak RSS 323352 KiB**. This runs the new admission file, existing -custom-function lifecycle and function-catalog ZMQ suites, and three affected -MCP/CLI regressions. It adds native ordinary/ancestor/file-symlink denial with -unchanged registry membership and sentinel bytes, PID-incarnation rejection, -independent native-server policy, exact destination/receipt validation, and -actual manager persistence plus lazy package reopen/invocation on a synthetic -2x2 array. Read-only destination routing is tested with unfinished catalog -preparation and asserts it is never started. The current CLI adapter derives -connection options/defaults from `ExecutionConnectionSpec` and the public -request factory. Its real `python -m openhcs.mcp.dev_client -register-custom-function --help` entrypoint succeeds and exposes those fields; -missing port is rejected before launching a client. - -One attempted extension of that shard could not collect -`test_function_step_transport.py::test_persisted_custom_function_is_importable_from_package`: -the module imports the unbuilt CellProfiler `_granularity_reconstruct` extension. -No tests ran in that failed invocation (exit4, 7.16 seconds, peak373668 KiB). -Its mocked XDG helper is updated to accept the new non-creating projection; -that module is **not claimed passed**. No extension build/install or collection -workaround is performed during the freeze. Subsequent isolated shard result is -the 94-test evidence above, not a claim that this missing dependency disappeared. - -Available RAM stayed above 8 GiB (latest13.4 GiB); guard still warns about -historical11.1 GiB swap. No heavy runtime or validation-lock holder was created. - -Owned disposable fixtures (now removed after all handles were terminal): -`/home/ts/.cache/agent-scratch/openhcs-registration-230-20260929/pytest`. -The final owned parent directory contained3.5 MiB of synthetic pytest fixtures; -it was deleted after retaining these results. Fixtures can be regenerated from -the tests. Source, receipts, original033 and the foreign saved file are preserved. -No MCP/JVM/GUI handle was started. Tests are source evidence, not installed or -live acceptance, and the frozen installed 17/228 tree remains untouched. - -Following #234 integration, isolated source environments explicitly pin -`POLYSTORE_IMAGEJ_CACHE_ROOT=/home/ts/.cache/polystore/imagej` and -`POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD=false` before imports. This is not a request -to download, copy or launch a bundle; no native runtime is started by these -admission tests. - -At merge5899d6323, with recorded PolyStore1209068 and the explicit cache policy, -the post-integration check runs only the22 admission cases plus the real native -control-router destination case: **23 passed, 6.18 seconds, peak287876 KiB**. -OpenHCS and PolyStore import paths resolve inside this isolated tree. AST parsing -of all15 changed Python source/test files passes; this is a focused source check, -not a full NRA scan or a global proof. - -Source commands used the shared Python executable, explicit worktree/submodule -PYTHONPATH, `OPENHCS_CPU_ONLY=true`, `PYTHONDONTWRITEBYTECODE=1`, and -`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`. Import OpenHCS before `pytest.main`; pass -`--noconftest --import-mode=importlib -p no:cacheprovider -o addopts= -q --tb=short` -and a basetemp below the owned scratch directory. The94-test selectors were: - -```text -tests/unit/agent/test_custom_registration_admission.py -tests/unit/test_custom_function_lifecycle.py -tests/unit/test_function_catalog_zmq.py -tests/unit/agent/test_mcp_server.py::test_mcp_register_custom_function_delegates_to_function_catalog_service -tests/unit/agent/test_mcp_server.py::test_mcp_dev_client_function_commands_project_tool_arguments -tests/unit/agent/test_mcp_server.py::test_mcp_dev_client_register_custom_function_renders_next_steps -``` - -All lightweight check handles are terminal; this worker has never acquired a -heavy runtime slot for230. No optional hosted CI is blocking the checkpoint. - -## Changed paths - -Production: - -- `openhcs/agent/capabilities.py` -- `openhcs/agent/dto/functions.py` -- `openhcs/agent/services/endpoint_function_catalog_service.py` -- `openhcs/agent/services/function_catalog_service.py` -- `openhcs/core/xdg_paths.py` -- `openhcs/mcp/context.py` -- `openhcs/mcp/dev_client_commands/knowledge_pipeline.py` -- `openhcs/processing/custom_functions/manager.py` -- `openhcs/runtime/zmq_control.py` -- `openhcs/runtime/zmq_execution_client.py` - -Tests: - -- `tests/unit/agent/test_custom_registration_admission.py` -- `tests/unit/agent/test_mcp_server.py` -- `tests/unit/test_custom_function_lifecycle.py` -- `tests/unit/test_function_catalog_zmq.py` -- `tests/unit/test_function_step_transport.py` (fixture projection only; module - collection blocked as recorded above) - -Receipt/body: `docs/plans/custom_registration_admission_20260929.md`. -Shared-owner coordination: `docs/plans/custom_registration_205_coordination.md`, -posted directly to the existing #205 owner at -https://github.com/OpenHCSDev/openhcs/pull/205#issuecomment-5900395883. -The local direct-DM route did not resolve this worker; no thread registration, -bus repair, agent restart or goal replay was attempted. Coordination scope is -the registration-specific DTO/service regions, not measurement declarations. - -## Actual pattern review (focused source, not a full NRA proof) - -- BOUND-2/BOUND-6: existing `AgentPathPolicy` owns write permission; - `CustomFunctionManager` and XDG helpers own paths. A different native storage - declaration changes that owner, not a new endpoint-to-filename registry. -- IMPL-12: replaced the manager's repeated name-to-file construction with its - `source_path_for_name`, consumed by admission, save, named load/read/delete, - revision checking (`require_source`) and both rename destinations. The six - remaining same-schema constructions were removed in the pre-live follow-up. - Five owner-interception cases witness that those named operations cannot - bypass it. Directory enumeration still reads actual persisted paths; it is - not name-to-file construction and has not been replaced by a filename roster. - New public names take the same helper, not a second source parser. -- MEMB-2/MEMB-3: the new control action declares its message on the existing - request/strategy family. Existing registration/discovery registries remain - derived; no companion roster, new runner or duplicate custom registry. -- IMPL-13: admission extends the existing DTO, endpoint service, typed control - protocol and atomic manager persistence, rather than forking registration. -- IDEN-8: destination admission and the mutation receipt reuse existing - `ProcessIdentity` (PID plus creation time), not bare PID/new UUID state. The - new-case witness changes creation time while retaining PID: native registration - rejects before manager evaluation. This is not authentication of hostile - servers or a proof that a remote filesystem is shared with the MCP process. -- BOUND-1/IMPL-13: the existing CLI's public factory/typed connection projects - the new route fields; no internal admission authority is generated as an - option and no alternate registration implementation is added. - -## Remaining acceptance (not closed) - -The current normal merge includes mainfbf6b2d91 (#225/#236), merge8fc0ecc63. -The authoring how-to now requires explicit reflected routing, deliberately -authored name and a caller-intended absolute store obtained through the native -Manager in the controlled server launch environment. It describes pre-dispatch -destination/incarnation proof and postdispatch uncertainty separately; a -receipt after writing is not admission. No arbitrary-server MCP destination -lookup is claimed. The existing knowledge manifest summary is updated; it -contains no checked-in document digest to duplicate. The empirical guide now -records parent's merged/installed #225 synthetic acceptance, not biology. - -`tests/diagnostics/check_custom_registration_live.py` is a reusable bounded -ordinary stdio MCP/native server journey, with a test-only delayed-response and -unsupported-proof seam through the existing server launch owner. It pins all -source imports, controlled XDG roots, shared-cache/download-false policy and -one native thread; acquires the shared nonblocking lock and records measured -call durations (not session ages), sentinels, original inputs and handles. -Unexpected calls are not retried. This prepared driver is not a passing receipt. - -Pre-live checkpoint9020cae18 is published in the existing draft233. New source -changes were completed before any live handle existed. Ruff on both diagnostic -files and the admission test, `git diff --check`, and the stdlib-only diagnostic -CLI help pass. At exact sourcec10bb3f85, the five new filename-owner cases -**pass in3.02 seconds**, peak183520 KiB RSS, with OpenHCS and PolyStore imports -verified inside this tree. They intercept `require_source`, named load/read, -delete and update before source reads/evaluation, proving use of the filename -owner. The earlier94/23 results do not cover this main merge. Exact command, -paths and output: `tests/runtime_diagnostics/registration_owner_five_20260929/` -(`receipt.json`, `pytest.txt`). Two pytest warnings concern disabled async -plugins' configuration options; these five cases are synchronous. - -The actual guard now warns about non-swap disk headroom (/home19.8 GiB), with -RAM13.0–13.6 GiB. Parent explicitly withdrew new heavy-runtime admission until -its own cleanup and recheck. No live process, JVM, GUI, registration call or -validation-lock holder was started here. The diagnostic rejects any non-swap -guard warning; source-only preparation/publication continues. Remaining named -dependency is parent's verified resource-headroom release, followed by this -source-pinned synthetic journey and parent-owned installation/acceptance. - -Parent explicitly authorized just these five bounded lightweight cases during -its cleanup. The single existing-interpreter test process used the nonblocking -validation lock, one native thread and no conftest/auto-loaded plugins. It is -terminal and the lock released. Guard at this test's dispatch showed RAM13.4 -GiB, /home20.1 GiB, and only historical-swap warning; this does **not** assume -parent's heavy-runtime slot release. Owned disposable source-test scratch: -`/home/ts/.cache/agent-scratch/openhcs-registration-owner-five-20260929`. - -After verified closure of the frozen author's owned MCP/viewer and release of -the technical slot, run controlled owned-vs-shared endpoint, escaping-path and -delayed-response checks, plus register/discover/compile/execute on a tiny -synthetic function through the actual installed MCP path. The integration owner -controls review, installation and slot handoff; no scientific execution, JVM, -installation or foreign namespace cleanup is authorized to this source worker. -This PR remains draft and references #230 without claiming closure. - -Ordinary persistence admission is not a sandbox for arbitrary authorized Python; -filesystem races and hostile remote servers are not claimed globally solved. -Broad #206/#16 preparation/ROI scope and its 26 combined-suite failures remain -in their separate trees/PRs. No ROI profile or preservation pass is invented. +# Custom-function admission and responsive preparation (#230) + +Implementation owner: Lovelace. Integration/install owner: coordinator. +Draft PR233: https://github.com/OpenHCSDev/openhcs/pull/233 +Source: /home/ts/wt/openhcs-custom-function-admission-20260929. +This checkpoint follows normal main238 integration f5dbebb27 (main01d1a8c55c); +recorded PolyStore1209068 is unchanged. No installed tree is edited. + +## Working source + +Registration requires an explicit reflected ExecutionConnectionSpec port, also +for ephemeral source. Persistence requires the deliberately authored function_name +and caller-intended absolute storage_dir. Existing AgentPathPolicy admits the +directory and Manager.source_path_for_name before client creation. A read-only +native destination request verifies that endpoint's actual Manager-owned store, +exact file, and ProcessIdentity (PID plus creation time), without evaluating source +or starting catalogue preparation. Unsupported or mismatched destinations reject +before source dispatch. The native service repeats caller/native write admission +before evaluation and atomic persistence. Post-write receipt checks are not admission. + +Cold catalogue/kernel warming belongs to the existing native FunctionCatalogPreparation +future, cancellation and supervised child from #238. New typed start/status/cancel +requests and nominal control strategies project that same owner promptly. They add +no process launcher, future store, cache roster or warmup registry. The handle is the +explicit connection plus native process incarnation; stale owners reject. Cancellation +signals that owner without joining inline; status observes terminal cleanup. + +Registration performs one bounded destination/readiness observation under the existing +control deadline. NOT_STARTED, PENDING, CANCELLING, FAILED or CANCELLED rejects before +any source-bearing registration RPC. The native mutation handler likewise refuses +cold source without initiating warming. Only READY permits one _send_control_request. +The previous mutation resend loop and inline catalogue polling were removed in place. +After source dispatch, timeout/error/malformed receipt stays uncertain; no replay, +fallback or timeout inflation. Pending preparation cannot write later on behalf of +a registration call whose observation expired. + +EndpointFunctionCatalogServiceABC composes the catalogue contract with these three +typed preparation operations; ZMQFunctionCatalogService implements it. Capabilities +use the context's typed endpoint_function_catalog dependency, not undeclared methods +on FunctionCatalogServiceABC. Normal factory and raw local MCP contexts share one +endpoint service for catalogue and preparation. Explicit endpoint injection is +supported. Hosted contexts retain explicitly injected native FunctionCatalogService; +compiler/server native catalogues acquire no second future. No NotImplemented stub, +getattr cast, isinstance dispatch, or second function registry was added. + +The canonical custom-function-authoring how-to now explains explicit routing, +controlled-launch Manager destination lookup, start/status/cancel handle fields, +READY-only mutation and precise postdispatch uncertainty. The knowledge manifest +summary/tags track this; document digests remain derived, not duplicated. + +## Actual evidence (source, not installed acceptance) + +Current focused provider-free shard: **47 passed, 25 deselected, 7.32 seconds**; +process elapsed8.13s, peak305992KiB, exit0. Existing shared Python, explicit reviewed +worktree plus eight submodule src paths, pinned shared bundle/downloadfalse, +threads1, plugins/conftest disabled, nonblocking validation lock. +OpenHCS and PolyStore imports resolve in this tree. Two warnings are disabled +async-plugin configuration options, not failed tests. +Receipt: tests/runtime_diagnostics/registration_readiness_20260929/. + +This shard covers all registration/admission cases, five Manager filename-owner +interceptions, generated MCP controls, real default/raw/explicit context construction, +responsive pending start/status/cancel with a controlled future, stale incarnation +rejection, failed preflight zero mutation, native cold-registration rejection, one +source-bearing client RPC on ready/pending/timeout, and #238 readiness/error propagation. +The controlled pending unit future is not the actual 53-second cold kernel job. + +Historical source checkpoints: 94 cases passed10.41s/323352KiB before newer merges; +23 cases passed6.18s/287876KiB after234; five filename cases passed3.02s/183520KiB at +c10bb3f85. These do not substitute for current live or full-suite evidence. + +The standard existing source abi3 extension was built with setup.py build_ext +--inplace --parallel1 and named owned build-temp/build-lib: **1.48s,93004KiB,exit0**. +Import receipt verifies the source binary45440bytes and source/binary hashes: +tests/runtime_diagnostics/registration_native_build_20260929/. +No install, build-dependency download, CellProfiler skip or replacement registry. +Installed225 already contains its own compiled extension; the previous absence +was in this source-only test tree, not a proved installed dependency defect. + +## Preserved failed live predecessor + +At exact81e636ad4, attempt01 used ordinary stdio MCP and three owned headless +synthetic endpoints. Health/context, missing-route, outside/file/ancestor symlink, +foreign store and unsupported-proof checks passed; protected sentinel was unchanged, +foreign/unsupported audits had zero source-bearing registration RPCs. Positive +call11 expired at10.011s and remained uncertain. Native audit showed automatic +mutation resends while cold preparation was pending. Same-handle read-only observation +exposed the unbuilt source extension. Compilation/execution and delayed acceptance +were not reached. This is **failed source-live evidence**, not acceptance. +tests/runtime_diagnostics/registration_live_20260929_attempt01/ retains original +arguments, failure, native control journals, stderr and close-owned disposition. +MCP and all three native handles are terminal; each server returned0, identities +were verified and the lock released. Original H002033 and foreign saved side effect +are preserved and never replayed. No science, shared7777 contact, GUI or JVM. + +## Focused architectural audit + +Scope: changed production declarations, callers and source tests; no full NRA scan +or global proof. Actual catalog witnesses: + +- IMPL-4: endpoint preparation contract now owns all three methods. Default/raw/ + explicit MCP construction cases execute the generated controls; native compiler/ + hosted catalogues remain their own complete family, with no dummy method bodies. +- IMPL-13: start/status/cancel extends the one native future/cancellation/child. + Repeated start coalesces the exact future/thread; failed readiness sends zero + register RPCs. Mutation no longer borrows the discovery retry mechanism. +- IMPL-12: Manager.source_path_for_name owns save/admission, named reads/deletes/ + updates, require_source and rename destinations. Five interception cases prove + those operations cannot bypass it. Directory enumeration reads real files and + remains enumeration, not a companion filename roster. +- MEMB-2/MEMB-3: control/capability membership derives from existing declarations; + each new request leaf owns its operation/strategy. A new case extends that family, + not an action switch or hand-maintained catalogue. +- IDEN-8: one handle and destination token reuse ProcessIdentity; changing creation + time while retaining PID rejects before cancellation/evaluation. +- BOUND-1/BOUND-2/BOUND-6: existing typed request/response codecs decode at control/ + MCP boundaries; native Manager and AgentPathPolicy remain path/write authorities. + No second destination resolver or source-code name parser. +- TIME-1/TIME-5: replaced registration polling was deleted in place. Existing + catalogue polling remains read-only discovery, not a legacy mutation path. + Delayed/unsupported fault injection lives only in the diagnostic driver. + +## Remaining live gates and resource bounds + +The diagnostic now freezes the reviewed source SHA and submodule pins, starts three +headless fixture endpoints with native threads1, limits each observation to10s and +the journey to240s, acquires nonblocking validation.lock, and rechecks actual RAM/ +disk plus scratch<80MiB before dispatch. It asserts **zero source-bearing register +RPCs before READY and exactly two total** (initial positive plus controlled delayed), +not merely one persistence marker. The complete journey is registration/discovery/ +compile/execution of an8x8 plus-three synthetic function, escaping-path sentinels, +foreign/unsupported predispatch denials and same-handle uncertain receipt reconciliation. + +--installed-entrypoint mode requires PYTHONPATH unset, cwd outside SOURCE, actual +OpenHCS/PolyStore imports matching the reviewed installed source, and the ordinary +McpDevServerSpec. It is implemented but not live-proved. Source mode is explicitly +labelled source_live_not_installed. Parent owns final merge/install and installed +acceptance; no frozen installation changes are authorised here. + +At the latest worker guard, /home19.9GiB and RAM12.1GiB add a non-swap disk warning. +No new heavy runtime starts under that warning. The bounded47-case source shard +allocated328KiB fixture scratch; it is terminal and released the lock. Pending +dependency: verified non-swap headroom release/recheck, then finite cold source-live +journey and parent installed acceptance. No optional hosted CI wait. + +Owned disposable build objects136KiB, filename-test fixtures40KiB and current +source-test fixtures328KiB were removed after retaining receipts. They are reproducible +from the tests/build command; the source in-place extension remains for acceptance. +Original attempt01 synthetic scratch remains identified separately from receipts/UNKNOWN input. + +This draft references #230, #238, #233 and does not claim Closes acceptance yet. +Broad #206/PolyStore16 scope and its26 combined-suite failures remain in their separate +trees/PRs; no ROI profile or preservation pass is invented. Ordinary write admission +is not a Python sandbox or proof against hostile remote servers/filesystem races. diff --git a/docs/source/development/mcp_knowledge_base_manifest.json b/docs/source/development/mcp_knowledge_base_manifest.json index e225663960..c14c2906e8 100644 --- a/docs/source/development/mcp_knowledge_base_manifest.json +++ b/docs/source/development/mcp_knowledge_base_manifest.json @@ -822,8 +822,8 @@ "document_id": "openhcs_custom_function_workflow", "section_count": 0, "source_path": "packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md", - "summary": "Add a missing operation through typed custom-function owners: explicit endpoint/name/store admission before dispatch, controlled-launch native destination proof, postdispatch uncertainty without replay, stable imports, compilation and bounded outputs; separate technical reuse from biological acceptance.", - "tags": ["missing analysis operation", "custom function workflow", "typed artifact authoring", "isolated catalog endpoint", "persisted custom import", "registration timeout reconcile", "ColumnarRows", "3D centres"], + "summary": "Add a missing operation through typed custom-function owners: explicit endpoint/name/store admission, responsive native catalogue preparation with an incarnation-bound start/status/cancel handle, single source dispatch only after readiness, postdispatch uncertainty without replay, stable imports, compilation and bounded outputs; separate technical reuse from biological acceptance.", + "tags": ["missing analysis operation", "custom function workflow", "typed artifact authoring", "isolated catalog endpoint", "catalog preparation readiness cancellation", "persisted custom import", "registration timeout reconcile", "ColumnarRows", "3D centres"], "title": "Add a missing analysis operation as a typed custom function" }, { diff --git a/openhcs/agent/capabilities.py b/openhcs/agent/capabilities.py index 1699d9052b..5aaf98279b 100644 --- a/openhcs/agent/capabilities.py +++ b/openhcs/agent/capabilities.py @@ -71,6 +71,8 @@ CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogPage, + FunctionCatalogPreparationHandle, + FunctionCatalogPreparationState, FunctionDetail, FunctionDetailRequest, FunctionSearchRequest, @@ -1891,6 +1893,57 @@ class DescribeFunctionCapability( ) +class StartFunctionCatalogPreparationCapability(FunctionCatalogCapability): + from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec + + name = "openhcs_start_function_catalog_preparation" + cli_command = "start-function-catalog-preparation" + kind = CapabilityKind.TOOL + title = "Start catalog preparation" + description = "Starts/coalesces existing native catalog/kernel preparation at an explicit owned port. Returns promptly with the exact process-incarnation handle; no custom source is submitted. Observe status, then register only once ready." + service = "endpoint_function_catalog" + mutating = True + side_effects = ("prepares_function_catalog", "writes_declared_kernel_caches") + input_contract = ExecutionConnectionSpec + output_contract = FunctionCatalogPreparationState + request_invocation = AgentDataclassRequestServiceInvocation( + service=lambda context: context.endpoint_function_catalog, + method=lambda service, request: service.start_catalog_preparation(request), + ) + + +class GetFunctionCatalogPreparationStatusCapability(FunctionCatalogCapability): + name = "openhcs_get_function_catalog_preparation_status" + cli_command = "get-function-catalog-preparation-status" + kind = CapabilityKind.TOOL + title = "Observe catalog preparation" + description = "Returns the existing preparation future's current state/progress promptly. Use the exact returned connection/process handle; stale owners reject without starting or replacing a runtime." + service = "endpoint_function_catalog" + input_contract = FunctionCatalogPreparationHandle + output_contract = FunctionCatalogPreparationState + request_invocation = AgentDataclassRequestServiceInvocation( + service=lambda context: context.endpoint_function_catalog, + method=lambda service, request: service.catalog_preparation_status(request), + ) + + +class CancelFunctionCatalogPreparationCapability(FunctionCatalogCapability): + name = "openhcs_cancel_function_catalog_preparation" + cli_command = "cancel-function-catalog-preparation" + kind = CapabilityKind.TOOL + title = "Cancel owned catalog preparation" + description = "Signals cancellation of the same incarnation-bound preparation future without blocking for child cleanup. Observe status until terminal; it does not restart preparation or submit custom source." + service = "endpoint_function_catalog" + mutating = True + side_effects = ("cancels_function_catalog_preparation",) + input_contract = FunctionCatalogPreparationHandle + output_contract = FunctionCatalogPreparationState + request_invocation = AgentDataclassRequestServiceInvocation( + service=lambda context: context.endpoint_function_catalog, + method=lambda service, request: service.cancel_catalog_preparation(request), + ) + + class RegisterCustomFunctionCapability(FunctionCatalogCapability): name = "openhcs_register_custom_function" cli_command = "register-custom-function" @@ -1901,7 +1954,8 @@ class RegisterCustomFunctionCapability(FunctionCatalogCapability): "source through CustomFunctionManager, then returns registry function_id " "values for MCP pipeline authoring. Requires an explicit execution port; " "persist=true also requires the endpoint's exact storage_dir and function_name " - "under AgentPathPolicy writable roots before dispatch. A transport timeout " + "under AgentPathPolicy writable roots before dispatch. Start/observe the native " + "catalog preparation handle first; not-ready registration rejects before source dispatch. A transport timeout " "is uncertain, not proof that no source or registry mutation occurred." ) service = "function_catalog" diff --git a/openhcs/agent/dto/functions.py b/openhcs/agent/dto/functions.py index df65397b99..9a8ec2df1f 100644 --- a/openhcs/agent/dto/functions.py +++ b/openhcs/agent/dto/functions.py @@ -17,6 +17,7 @@ from openhcs.agent.dto.common import SCHEMA_VERSION, AgentResultEnvelope from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec +from openhcs.agent.exceptions import AgentFacingErrorMixin from openhcs.agent.path_policy import AgentPathPolicy from openhcs.core.artifacts import ArtifactViewerStreaming from openhcs.core.function_reference import FunctionReference @@ -33,6 +34,9 @@ class FunctionCatalogControlMessageType(str, Enum): READ_REFERENCE = "openhcs_function_reference_read" REGISTER_CUSTOM = "openhcs_custom_function_register" CUSTOM_REGISTRATION_DESTINATION = "openhcs_custom_function_registration_destination" + START_PREPARATION = "openhcs_function_catalog_prepare" + READ_PREPARATION = "openhcs_function_catalog_preparation_status" + CANCEL_PREPARATION = "openhcs_function_catalog_preparation_cancel" class FunctionCatalogControlField(str, Enum): @@ -99,6 +103,83 @@ class FunctionSearchRequest(FunctionCatalogControlRequestABC): message_type = FunctionCatalogControlMessageType.SEARCH_CATALOG +@dataclass(frozen=True, slots=True) +class FunctionCatalogPreparationHandle: + """The one preparation owner in this exact execution-server incarnation.""" + + connection: ExecutionConnectionSpec + server_identity: ProcessIdentity + + def require_current_owner(self) -> None: + if self.server_identity != ProcessIdentity.current(): + raise RuntimeError( + "Function catalog preparation owner changed; handle is stale." + ) + + +class FunctionCatalogPreparationOutcome(str, Enum): + NOT_STARTED = "not_started" + PENDING = "pending" + CANCELLING = "cancelling" + READY = "ready" + FAILED = "failed" + CANCELLED = "cancelled" + + @property + def ready(self) -> bool: + return self is self.READY + + @property + def terminal(self) -> bool: + return self in (self.READY, self.FAILED, self.CANCELLED) + + +@dataclass(frozen=True, kw_only=True) +class FunctionCatalogPreparationState(AgentResultEnvelope): + handle: FunctionCatalogPreparationHandle + outcome: FunctionCatalogPreparationOutcome + progress: EndpointStartupStatus + + def require_ready(self) -> None: + if not self.outcome.ready: + raise FunctionCatalogNotReadyError(self) + + def require_handle(self, expected: FunctionCatalogPreparationHandle) -> None: + if self.handle != expected: + raise RuntimeError( + "Function catalog preparation response changed owner/connection." + ) + + +class FunctionCatalogNotReadyError(AgentFacingErrorMixin, RuntimeError): + agent_error_code = "function_catalog_not_ready" + agent_error_hint = "Start/observe the same typed catalog preparation handle; no source was dispatched." + + def __init__(self, state: FunctionCatalogPreparationState) -> None: + self.preparation = state + super().__init__( + f"Catalog preparation {state.handle!r} is {state.outcome.value}: {state.progress.message}. No source was dispatched." + ) + + +@dataclass(frozen=True, slots=True) +class FunctionCatalogPreparationStartRequest(FunctionCatalogControlRequestABC): + connection: ExecutionConnectionSpec + message_type = FunctionCatalogControlMessageType.START_PREPARATION + + +@dataclass(frozen=True, slots=True) +class FunctionCatalogPreparationStatusRequest(FunctionCatalogControlRequestABC): + handle: FunctionCatalogPreparationHandle + message_type = FunctionCatalogControlMessageType.READ_PREPARATION + + +@dataclass(frozen=True, slots=True) +class FunctionCatalogPreparationCancelRequest(FunctionCatalogControlRequestABC): + handle: FunctionCatalogPreparationHandle + message_type = FunctionCatalogControlMessageType.CANCEL_PREPARATION + + @dataclass(frozen=True, slots=True) class FunctionDetailRequest(FunctionIdentity): max_doc_chars: int | None = DEFAULT_FUNCTION_DETAIL_DOC_CHARS @@ -493,6 +574,13 @@ def catalog(self) -> FunctionCatalogPage: return self.value +class FunctionCatalogPreparationStateControlResponse( + FunctionCatalogControlResponseBase[FunctionCatalogPreparationState] +): + field = FunctionCatalogControlField.PREPARATION + value_type = FunctionCatalogPreparationState + + class FunctionCatalogPreparationStatus(str, Enum): """Control response state while endpoint catalog preparation is active.""" diff --git a/openhcs/agent/services/endpoint_function_catalog_service.py b/openhcs/agent/services/endpoint_function_catalog_service.py index fc1c5886f0..b7b6f42fff 100644 --- a/openhcs/agent/services/endpoint_function_catalog_service.py +++ b/openhcs/agent/services/endpoint_function_catalog_service.py @@ -4,6 +4,7 @@ import logging import threading +from abc import abstractmethod from collections.abc import Callable, Mapping from concurrent.futures import CancelledError, Future from dataclasses import dataclass, replace @@ -12,6 +13,7 @@ from zmqruntime import OperationCancellation, OperationDeadline +from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec from openhcs.agent.dto.functions import ( DEFAULT_FUNCTION_DETAIL_DOC_CHARS, CustomFunctionRegistrationDestinationRequest, @@ -20,6 +22,11 @@ FunctionCatalogControlRequest, FunctionCatalogEntry, FunctionCatalogPage, + FunctionCatalogPreparationCancelRequest, + FunctionCatalogPreparationHandle, + FunctionCatalogPreparationStartRequest, + FunctionCatalogPreparationState, + FunctionCatalogPreparationStatusRequest, FunctionDetail, FunctionDetailControlRequest, FunctionReferenceControlRequest, @@ -174,7 +181,29 @@ def cancel_and_join(self) -> None: self.thread.join() -class ZMQFunctionCatalogService(FunctionCatalogServiceABC): +class EndpointFunctionCatalogServiceABC(FunctionCatalogServiceABC): + """Endpoint catalog with responsive access to its native preparation owner.""" + + @abstractmethod + def start_catalog_preparation( + self, connection: ExecutionConnectionSpec + ) -> FunctionCatalogPreparationState: + """Start/coalesce the selected endpoint's existing preparation future.""" + + @abstractmethod + def catalog_preparation_status( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + """Observe the exact native incarnation without starting or waiting.""" + + @abstractmethod + def cancel_catalog_preparation( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + """Signal the exact native preparation owner without joining its child.""" + + +class ZMQFunctionCatalogService(EndpointFunctionCatalogServiceABC): """Use one execution endpoint as the authoring callable-catalog authority.""" def __init__( @@ -369,19 +398,16 @@ def register_custom_function( """Register source at the endpoint and project ephemeral source locally.""" request = request.admitted(self._path_policy) - endpoint = replace( - self._config_provider(), - default_port=request.connection.require_port("Custom function registration"), - client_host=request.connection.host, - transport_mode=request.connection.transport_endpoint().transport_mode, - persistent=request.connection.persistent, - ) + endpoint = self._endpoint_for_connection(request.connection) deadline = OperationDeadline.after_milliseconds( - endpoint.control_timeout_ms, operation="custom function registration", + endpoint.control_timeout_ms, + operation="custom function registration", ) client = self._client_for(endpoint) destination = client.custom_function_registration_destination( - CustomFunctionRegistrationDestinationRequest(function_name=request.function_name), + CustomFunctionRegistrationDestinationRequest( + function_name=request.function_name + ), operation_deadline=deadline, ) destination.require_request(request) @@ -391,15 +417,21 @@ def register_custom_function( request = replace(request, server_identity=destination.server_identity) self.invalidate() self._config_provider = lambda: endpoint - # The existing read-only owner may poll preparation, but it must not - # carry executable source. Failure here precedes mutation dispatch. - client.get_function_catalog( - FunctionCatalogControlRequest(compact_signatures=request.compact_signatures), + # Observe the existing future once, never inline-poll a cold warmup. + handle = FunctionCatalogPreparationHandle( + request.connection, destination.server_identity + ) + state = client.function_catalog_preparation( + FunctionCatalogPreparationStatusRequest(handle), operation_deadline=deadline, ) + state.require_handle(handle) + state.require_ready() deadline.remaining_seconds() try: - result = client.register_custom_function(request, operation_deadline=deadline) + result = client.register_custom_function( + request, operation_deadline=deadline + ) destination.require_result(request, result) if not request.persist: from openhcs.processing.custom_functions.manager import ( @@ -417,6 +449,55 @@ def register_custom_function( raise CustomFunctionRegistrationUncertainError(request) from error return result + def _endpoint_for_connection( + self, connection: ExecutionConnectionSpec + ) -> OpenHCSZMQConfig: + """Use the explicit typed route, never a companion endpoint registry.""" + return replace( + self._config_provider(), + default_port=connection.require_port("Function catalog operation"), + client_host=connection.host, + transport_mode=connection.transport_endpoint().transport_mode, + persistent=connection.persistent, + ) + + def start_catalog_preparation( + self, connection: ExecutionConnectionSpec + ) -> FunctionCatalogPreparationState: + endpoint = self._endpoint_for_connection(connection) + state = self._client_for(endpoint).function_catalog_preparation( + FunctionCatalogPreparationStartRequest(connection), + ) + if state.handle.connection != connection: + raise RuntimeError( + "Catalog preparation response changed the explicit connection." + ) + self.invalidate() + self._config_provider = lambda: endpoint + return state + + def catalog_preparation_status( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + state = self._client_for( + self._endpoint_for_connection(handle.connection) + ).function_catalog_preparation( + FunctionCatalogPreparationStatusRequest(handle), + ) + state.require_handle(handle) + return state + + def cancel_catalog_preparation( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + state = self._client_for( + self._endpoint_for_connection(handle.connection) + ).function_catalog_preparation( + FunctionCatalogPreparationCancelRequest(handle), + ) + state.require_handle(handle) + return state + def invalidate(self) -> None: """Discard the derived page; the next read requests the endpoint again.""" diff --git a/openhcs/mcp/context.py b/openhcs/mcp/context.py index 165795e1cd..26c8fd70ea 100644 --- a/openhcs/mcp/context.py +++ b/openhcs/mcp/context.py @@ -11,6 +11,9 @@ ArchitectureProjectionService, ) from openhcs.agent.services.config_service import ConfigService + from openhcs.agent.services.endpoint_function_catalog_service import ( + EndpointFunctionCatalogServiceABC, + ) from openhcs.agent.services.execution_session_service import ExecutionSessionService from openhcs.agent.services.function_catalog_service import ( FunctionCatalogServiceABC, @@ -26,14 +29,14 @@ from openhcs.agent.services.plate_inspection_service import PlateInspectionService from openhcs.agent.services.plate_streaming_service import PlateStreamingService from openhcs.agent.services.runtime_server_service import RuntimeServerService - from openhcs.agent.services.viewer_endpoint_discovery import ( - ViewerEndpointDiscoveryService, - ) from openhcs.agent.services.selected_plate_service import SelectedPlateService from openhcs.agent.services.synthetic_plate_service import ( SyntheticPlateGenerationService, ) from openhcs.agent.services.ui_bridge_service import UiBridgeService + from openhcs.agent.services.viewer_endpoint_discovery import ( + ViewerEndpointDiscoveryService, + ) from openhcs.agent.services.viewer_window_service import ViewerWindowService @@ -46,6 +49,7 @@ class OpenHCSAgentContext: "_authoring_context_service", "_config_service", "_execution_service", + "_endpoint_function_catalog", "_function_catalog", "_knowledge_base_service", "_object_state_field_help_service", @@ -69,6 +73,7 @@ def __init__( config_service: "ConfigService | None" = None, execution_service: "ExecutionSessionService | None" = None, function_catalog: "FunctionCatalogServiceABC | None" = None, + endpoint_function_catalog: "EndpointFunctionCatalogServiceABC | None" = None, knowledge_base_service: "KnowledgeBaseService | None" = None, object_state_field_help_service: "ObjectStateFieldHelpService | None" = None, pipeline_service: "PipelineAuthoringService | None" = None, @@ -87,6 +92,7 @@ def __init__( self._config_service = config_service self._execution_service = execution_service self._function_catalog = function_catalog + self._endpoint_function_catalog = endpoint_function_catalog self._knowledge_base_service = knowledge_base_service self._object_state_field_help_service = object_state_field_help_service self._pipeline_service = pipeline_service @@ -112,12 +118,23 @@ def viewer_endpoint_discovery_service(self) -> "ViewerEndpointDiscoveryService": @property def function_catalog(self) -> "FunctionCatalogServiceABC": if self._function_catalog is None: - from openhcs.agent.services.function_catalog_service import ( - FunctionCatalogService, + self._function_catalog = self.endpoint_function_catalog + return self._function_catalog + + @property + def endpoint_function_catalog(self) -> "EndpointFunctionCatalogServiceABC": + """Compose one typed endpoint authority for local catalog/control tools.""" + if self._endpoint_function_catalog is None: + from openhcs.agent.services.endpoint_function_catalog_service import ( + ZMQFunctionCatalogService, ) + from openhcs.pyqt_gui.config import load_cached_ui_execution_endpoint_sync - self._function_catalog = FunctionCatalogService(path_policy=self.path_policy) - return self._function_catalog + endpoint = load_cached_ui_execution_endpoint_sync() + self._endpoint_function_catalog = ZMQFunctionCatalogService( + lambda: endpoint, path_policy=self.path_policy, + ) + return self._endpoint_function_catalog @property def config_service(self) -> "ConfigService": @@ -287,15 +304,19 @@ def create_agent_context() -> OpenHCSAgentContext: endpoint_config = load_cached_ui_execution_endpoint_sync() path_policy = AgentPathPolicy.from_environment() + catalog = ZMQFunctionCatalogService( + lambda: endpoint_config, path_policy=path_policy, + ) return OpenHCSAgentContext( path_policy=path_policy, - function_catalog=ZMQFunctionCatalogService( - lambda: endpoint_config, path_policy=path_policy, - ), + function_catalog=catalog, + endpoint_function_catalog=catalog, ) def create_hosted_agent_context() -> OpenHCSAgentContext: """Create the self-contained catalog authority used by hosted MCP.""" - return OpenHCSAgentContext() + from openhcs.agent.services.function_catalog_service import FunctionCatalogService + + return OpenHCSAgentContext(function_catalog=FunctionCatalogService()) diff --git a/openhcs/runtime/function_catalog_preparation.py b/openhcs/runtime/function_catalog_preparation.py index 6044a13df6..09c780fdfa 100644 --- a/openhcs/runtime/function_catalog_preparation.py +++ b/openhcs/runtime/function_catalog_preparation.py @@ -14,6 +14,11 @@ from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus if TYPE_CHECKING: + from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec + from openhcs.agent.dto.functions import ( + FunctionCatalogPreparationHandle, + FunctionCatalogPreparationState, + ) from openhcs.agent.services.function_catalog_service import ( FunctionCatalogServiceABC, ) @@ -114,11 +119,82 @@ def snapshot(self) -> EndpointStartupStatus: with self._lock: return self._snapshot - def _set_message(self, message: str) -> None: + def start( + self, connection: ExecutionConnectionSpec + ) -> FunctionCatalogPreparationState: + """Start/coalesce the existing future and return its responsive handle.""" + from zmqruntime.messages import ProcessIdentity + + from openhcs.agent.dto.functions import FunctionCatalogPreparationHandle + + connection.require_port("Function catalog preparation") + handle = FunctionCatalogPreparationHandle(connection, ProcessIdentity.current()) + self.ensure_started() + return self.observe(handle) + + def observe( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + """Project this future, never wait, restart or build another job store.""" + from openhcs.agent.dto.common import SCHEMA_VERSION, AgentError + from openhcs.agent.dto.functions import ( + FunctionCatalogPreparationOutcome as Outcome, + ) + from openhcs.agent.dto.functions import ( + FunctionCatalogPreparationState, + ) + + handle.require_current_owner() + errors = () + with self._lock: + future, snapshot = self._future, self._snapshot + if future is None: + outcome = Outcome.NOT_STARTED + elif future.cancelled(): + outcome = Outcome.CANCELLED + elif not future.done(): + outcome = ( + Outcome.CANCELLING + if self._cancellation.requested() + else Outcome.PENDING + ) + elif (error := future.exception()) is not None: + outcome = Outcome.FAILED + errors = ( + AgentError( + code="function_catalog_preparation_failed", + message=str(error), + exception_type=type(error).__name__, + ), + ) + else: + outcome = Outcome.READY + return FunctionCatalogPreparationState( + schema_version=SCHEMA_VERSION, + handle=handle, + outcome=outcome, + progress=snapshot, + errors=errors, + ) + + def cancel_preparation( + self, handle: FunctionCatalogPreparationHandle + ) -> FunctionCatalogPreparationState: + """Signal the same owner promptly; its thread unwinds the supervised child.""" + handle.require_current_owner() + self._cancellation.cancel() + return self.observe(handle) + + def _set_message( + self, + message: str, + *, + phase: EndpointStartupPhase = EndpointStartupPhase.PREPARING_CAPABILITIES, + ) -> None: with self._lock: self._snapshot = EndpointStartupStatus( sequence=self._snapshot.sequence + 1, - phase=EndpointStartupPhase.PREPARING_CAPABILITIES, + phase=phase, message=message, timestamp=time.time(), ) @@ -130,8 +206,16 @@ def _prepare(self, future: Future[None]) -> None: cancellation=self._cancellation, ) except CancelledError: + self._set_message( + "Function catalog preparation cancelled", + phase=EndpointStartupPhase.FAILED, + ) future.cancel() except BaseException as error: + self._set_message(str(error), phase=EndpointStartupPhase.FAILED) future.set_exception(error) else: + self._set_message( + "Function catalog ready", phase=EndpointStartupPhase.CONNECTED + ) future.set_result(None) diff --git a/openhcs/runtime/zmq_control.py b/openhcs/runtime/zmq_control.py index 4c507ad703..c2c0996546 100644 --- a/openhcs/runtime/zmq_control.py +++ b/openhcs/runtime/zmq_control.py @@ -6,12 +6,20 @@ from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar from metaclass_registry import AutoRegisterMeta, RegistryFamily, RegistryKeyAttribute from zmqruntime.messages import MessageFields, ResponseType +from openhcs.agent.dto.functions import FunctionCatalogControlRequestABC + if TYPE_CHECKING: + from openhcs.agent.dto.functions import ( + FunctionCatalogPreparationCancelRequest, + FunctionCatalogPreparationStartRequest, + FunctionCatalogPreparationState, + FunctionCatalogPreparationStatusRequest, + ) from openhcs.agent.services.function_catalog_service import FunctionCatalogService from openhcs.runtime.function_catalog_preparation import FunctionCatalogPreparation from openhcs.runtime.zmq_compilation import ZMQCompileArtifactRecord @@ -316,18 +324,111 @@ class CustomFunctionRegistrationDestinationMessageStrategy(ZMQControlMessageStra registry_key = request_type.message_type.value def handle(self, message: dict, context: ZMQControlRequestContext) -> dict: - from openhcs.agent.dto.functions import CustomFunctionRegistrationDestinationControlResponse + from openhcs.agent.dto.functions import ( + CustomFunctionRegistrationDestinationControlResponse, + ) try: request = self.request_type.from_control_payload(message) return CustomFunctionRegistrationDestinationControlResponse( - value=context.require_function_catalog().custom_function_registration_destination(request), + value=context.require_function_catalog().custom_function_registration_destination( + request + ), ).to_control_response() except Exception as error: return self.error_response(error) -class CustomFunctionRegistrationMessageStrategy(FunctionCatalogMessageStrategy): +PreparationRequestT = TypeVar( + "PreparationRequestT", bound=FunctionCatalogControlRequestABC +) + + +class FunctionCatalogPreparationMessageStrategy( + ZMQControlMessageStrategy, + Generic[PreparationRequestT], +): + """Responsive projection of the existing native preparation owner.""" + + request_type: ClassVar[type[FunctionCatalogControlRequestABC]] + + def handle(self, message: dict, context: ZMQControlRequestContext) -> dict: + from openhcs.agent.dto.functions import ( + FunctionCatalogPreparationStateControlResponse, + ) + + try: + request = self.request_type.from_control_payload(message) + return FunctionCatalogPreparationStateControlResponse( + value=self.preparation_state( + context.require_function_catalog_preparation(), request + ), + ).to_control_response() + except Exception as error: + return self.error_response(error) + + @abstractmethod + def preparation_state( + self, + preparation: FunctionCatalogPreparation, + request: PreparationRequestT, + ) -> FunctionCatalogPreparationState: + """Each request owns its operation, not a string action bag.""" + + +class StartFunctionCatalogPreparationStrategy( + FunctionCatalogPreparationMessageStrategy["FunctionCatalogPreparationStartRequest"], +): + from openhcs.agent.dto.functions import FunctionCatalogPreparationStartRequest + + request_type = FunctionCatalogPreparationStartRequest + registry_key = request_type.message_type.value + + def preparation_state( + self, + preparation: FunctionCatalogPreparation, + request: FunctionCatalogPreparationStartRequest, + ) -> FunctionCatalogPreparationState: + return preparation.start(request.connection) + + +class ReadFunctionCatalogPreparationStrategy( + FunctionCatalogPreparationMessageStrategy[ + "FunctionCatalogPreparationStatusRequest" + ], +): + from openhcs.agent.dto.functions import FunctionCatalogPreparationStatusRequest + + request_type = FunctionCatalogPreparationStatusRequest + registry_key = request_type.message_type.value + + def preparation_state( + self, + preparation: FunctionCatalogPreparation, + request: FunctionCatalogPreparationStatusRequest, + ) -> FunctionCatalogPreparationState: + return preparation.observe(request.handle) + + +class CancelFunctionCatalogPreparationStrategy( + FunctionCatalogPreparationMessageStrategy[ + "FunctionCatalogPreparationCancelRequest" + ], +): + from openhcs.agent.dto.functions import FunctionCatalogPreparationCancelRequest + + request_type = FunctionCatalogPreparationCancelRequest + registry_key = request_type.message_type.value + + def preparation_state( + self, + preparation: FunctionCatalogPreparation, + request: FunctionCatalogPreparationCancelRequest, + ) -> FunctionCatalogPreparationState: + return preparation.cancel_preparation(request.handle) + + +class CustomFunctionRegistrationMessageStrategy(ZMQControlMessageStrategy): """Register custom source through the execution endpoint catalog owner.""" from openhcs.agent.dto.functions import CustomFunctionRegistrationRequest @@ -335,19 +436,21 @@ class CustomFunctionRegistrationMessageStrategy(FunctionCatalogMessageStrategy): request_type = CustomFunctionRegistrationRequest registry_key = request_type.message_type.value - def handle_ready( - self, - message: dict, - function_catalog: "FunctionCatalogService", - ) -> dict: + def handle(self, message: dict, context: ZMQControlRequestContext) -> dict: from openhcs.agent.dto.functions import ( CustomFunctionRegistrationControlResponse, + FunctionCatalogPreparationHandle, ) - - request = self.request_type.from_control_payload(message) - return CustomFunctionRegistrationControlResponse( - value=function_catalog.register_custom_function(request), - ).to_control_response() + try: + request = self.request_type.from_control_payload(message) + context.require_function_catalog_preparation().observe( + FunctionCatalogPreparationHandle(request.connection, request.require_server_identity()) + ).require_ready() + return CustomFunctionRegistrationControlResponse( + value=context.require_function_catalog().register_custom_function(request), + ).to_control_response() + except Exception as error: + return self.error_response(error) class DebugSnapshotReadMessageStrategy( diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 9abeb0ebe1..348c3d6560 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -75,7 +75,9 @@ CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogControlRequest, + FunctionCatalogControlRequestABC, FunctionCatalogPage, + FunctionCatalogPreparationState, FunctionDetail, FunctionDetailControlRequest, FunctionReferenceControlRequest, @@ -814,7 +816,6 @@ def get_function_catalog( request: FunctionCatalogControlRequest, *, cancellation: OperationCancellation | None = None, - operation_deadline: OperationDeadline | None = None, ) -> FunctionCatalogPage: """Read the authoritative callable catalog from this execution endpoint.""" @@ -829,14 +830,12 @@ def get_function_catalog( ) if not connection_attempt.connect( EndpointConnectionPolicy.ATTACH_OR_START, - (self.config.client_connect_timeout_seconds if operation_deadline is None - else operation_deadline.cap_seconds(self.config.client_connect_timeout_seconds)), + self.config.client_connect_timeout_seconds, ): raise RuntimeError("Failed to connect to execution server") response = self._send_function_catalog_control_request( FunctionCatalogControlPayload.from_request(request).to_dict(), cancellation=cancellation, - operation_deadline=operation_deadline, ) return FunctionCatalogControlResponse.from_control_response(response).catalog @@ -899,7 +898,8 @@ def get_function_reference( def register_custom_function( self, request: CustomFunctionRegistrationRequest, - *, operation_deadline: OperationDeadline | None = None, + *, + operation_deadline: OperationDeadline | None = None, ) -> CustomFunctionRegistrationResult: """Send one mutation; readiness belongs to preceding read-only discovery. @@ -912,18 +912,27 @@ def register_custom_function( FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect(operation_deadline=operation_deadline): + if not self.is_connected() and not self.connect( + operation_deadline=operation_deadline + ): raise RuntimeError("Failed to connect to execution server") payload = FunctionCatalogControlPayload.from_request(request).to_dict() - response = (self._send_control_request(payload) if operation_deadline is None - else self._send_control_request(payload, timeout_ms=operation_deadline.remaining_milliseconds())) + response = ( + self._send_control_request(payload) + if operation_deadline is None + else self._send_control_request( + payload, timeout_ms=operation_deadline.remaining_milliseconds() + ) + ) return CustomFunctionRegistrationControlResponse.from_control_response( response ).result def custom_function_registration_destination( - self, request: CustomFunctionRegistrationDestinationRequest, - *, operation_deadline: OperationDeadline | None = None, + self, + request: CustomFunctionRegistrationDestinationRequest, + *, + operation_deadline: OperationDeadline | None = None, ) -> CustomFunctionRegistrationDestination: """Require the selected endpoint's native admission contract before mutation.""" from openhcs.agent.dto.functions import ( @@ -931,19 +940,59 @@ def custom_function_registration_destination( FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect(operation_deadline=operation_deadline): + if not self.is_connected() and not self.connect( + operation_deadline=operation_deadline + ): raise RuntimeError("Failed to connect to execution server") payload = FunctionCatalogControlPayload.from_request(request).to_dict() - response = (self._send_control_request(payload) if operation_deadline is None - else self._send_control_request(payload, timeout_ms=operation_deadline.remaining_milliseconds())) - return CustomFunctionRegistrationDestinationControlResponse.from_control_response(response).destination + response = ( + self._send_control_request(payload) + if operation_deadline is None + else self._send_control_request( + payload, timeout_ms=operation_deadline.remaining_milliseconds() + ) + ) + return ( + CustomFunctionRegistrationDestinationControlResponse.from_control_response( + response + ).destination + ) + + def function_catalog_preparation( + self, + request: FunctionCatalogControlRequestABC, + *, + operation_deadline: OperationDeadline | None = None, + ) -> FunctionCatalogPreparationState: + """One responsive start/status/cancel exchange on an existing endpoint.""" + from openhcs.agent.dto.functions import ( + FunctionCatalogControlPayload, + FunctionCatalogPreparationStateControlResponse, + ) + + deadline = operation_deadline or OperationDeadline.after_milliseconds( + self.config.control_timeout_ms, + operation="function catalog preparation observation", + ) + if not self.is_connected() and not self.connect_existing( + timeout=deadline.cap_seconds(1.0) + ): + raise RuntimeError( + "Function catalog preparation requires an existing execution endpoint." + ) + response = self._send_control_request( + FunctionCatalogControlPayload.from_request(request).to_dict(), + timeout_ms=deadline.remaining_milliseconds(), + ) + return FunctionCatalogPreparationStateControlResponse.from_control_response( + response + ).value def _send_function_catalog_control_request( self, request: dict, *, cancellation: OperationCancellation | None = None, - operation_deadline: OperationDeadline | None = None, ) -> dict: """Poll read-only discovery while endpoint catalog preparation is active.""" @@ -956,8 +1005,7 @@ def _send_function_catalog_control_request( while True: if cancellation.requested(): raise CancelledError("Function catalog preparation was cancelled") - response = (self._send_control_request(request) if operation_deadline is None - else self._send_control_request(request, timeout_ms=operation_deadline.remaining_milliseconds())) + response = self._send_control_request(request) pending = FunctionCatalogPreparationControlResponse.from_control_response( response ) @@ -974,9 +1022,7 @@ def _send_function_catalog_control_request( pending.status.phase, pending.status.message, ) - wait_seconds = (pending.retry_after_seconds if operation_deadline is None - else operation_deadline.cap_seconds(pending.retry_after_seconds)) - if cancellation.wait(wait_seconds): + if cancellation.wait(pending.retry_after_seconds): raise CancelledError("Function catalog preparation was cancelled") def send_debug_worker_command( diff --git a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md index 2c7ba6d581..c9236689fa 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md +++ b/packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md @@ -96,12 +96,35 @@ evaluating the source. The execution service independently admits the native write before evaluation and persistence. Receipt checks after writing verify the outcome; they are not write admission or a Python sandbox. -Complete read-only catalog discovery on the intended endpoint before mutation. -Cold preparation failures belong to that boundary: the service prepares via -the existing read-only catalog request, then sends registration exactly once. -It never polls/resends a source-bearing request, even if a server responds -preparation-pending. A read-only observation timeout does not justify a new -mutation; retain its handle/progress and reconcile that preparation first. +Prepare the catalogue on the intended endpoint **before submitting source**: + +1. Call `openhcs_start_function_catalog_preparation` with the same explicit + `port`, `host`, `transport_mode` and `persistent` connection fields. It starts + or coalesces the endpoint's existing catalogue/kernel preparation future, + including its supervised preparation child and declared kernel-cache writes. + It returns promptly, not after the cold preparation finishes. +2. Retain the returned `handle` exactly: its `connection` and + `server_identity` (PID plus creation time) identify that native owner. + Pass those two fields to `openhcs_get_function_catalog_preparation_status`. + Observe `outcome` and `progress`; do not infer readiness from elapsed time or + a progress message. Only `outcome="ready"` admits registration. Pending + observations do not start another future or submit custom source. +3. If preparation fails or is cancelled, retain the error and handle. Do not + submit source or replace the runtime implicitly. To cancel your pending + operation, pass the same fields to `openhcs_cancel_function_catalog_preparation`; + it signals the existing owner promptly. Continue observing that handle until + terminal, preserving the supervised child's cleanup rather than restarting it. +4. Once ready, complete catalogue discovery on the selected endpoint, then + send registration once. Registration independently observes readiness once + within the existing control deadline. `function_catalog_not_ready` means no + source-bearing registration RPC was sent by this service; the native handler + also refuses cold registration without initiating preparation. It does not + poll a cold catalogue and write later after the caller's observation expires. + +The service never polls/resends a source-bearing request, including on pending, +error or missing receipt. A start/status observation timeout is not proof that +preparation did not start or finish: preserve its input/handle and reconcile +the same owner. It does not authorise a new source mutation or runtime fallback. For isolated sessions, also pin `OPENHCS_UI_CONFIG_CACHE_FILE` before MCP startup and verify the selected catalog. That launch-time selector still owns diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 10c8d01431..e937094d4d 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -301,6 +301,8 @@ def save(): CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, FunctionCatalogPage, + FunctionCatalogPreparationOutcome, + FunctionCatalogPreparationState, FunctionDetail, ) @@ -532,7 +534,51 @@ def finish_job(ref): "sha256_before_after": sentinel_hash, "unchanged": True, } - call("openhcs_search_functions", {"query": "registration_live_probe", "limit": 5}, FunctionCatalogPage) + register(error_code="function_catalog_not_ready") + preparation_started = time.monotonic() + state = call( + "openhcs_start_function_catalog_preparation", + { + "port": args.port, + "host": "127.0.0.1", + "transport_mode": "tcp", + }, + FunctionCatalogPreparationState, + ) + receipt["preparation_handle"] = to_jsonable(state.handle) + preparation_deadline = time.monotonic() + 100 + if not state.outcome.ready: + register(error_code="function_catalog_not_ready") + while not state.outcome.ready: + assert not state.outcome.terminal, state + if time.monotonic() >= preparation_deadline: + raise RuntimeError( + "Preparation observation budget exhausted; preserve exact handle." + ) + time.sleep(1) + state = call( + "openhcs_get_function_catalog_preparation_status", + to_jsonable(state.handle), + FunctionCatalogPreparationState, + ) + assert state.outcome is FunctionCatalogPreparationOutcome.READY + receipt["cold_preparation_elapsed_seconds"] = ( + time.monotonic() - preparation_started + ) + audit_before_mutation = [ + json.loads(row) + for row in (receipt_dir / "owned-control.jsonl").read_text().splitlines() + ] + assert all( + row.get("type") != CustomFunctionRegistrationRequest.message_type.value + for row in audit_before_mutation + ) + receipt["zero_source_dispatch_before_ready"] = True + call( + "openhcs_search_functions", + {"query": "registration_live_probe", "limit": 5}, + FunctionCatalogPage, + ) registration = register() assert registration.connection.port == args.port assert registration.server_identity.pid == servers[0][1].pid @@ -670,6 +716,13 @@ def finish_job(ref): json.loads(row) for row in (receipt_dir / "owned-control.jsonl").read_text().splitlines() ] + assert ( + sum( + row.get("type") == CustomFunctionRegistrationRequest.message_type.value + for row in audit_rows + ) + == 2 + ), "Exactly one initial and one controlled-delayed source dispatch; no resend" assert ( len( [ @@ -682,6 +735,7 @@ def finish_job(ref): ) receipt.update( accepted=True, + source_bearing_register_rpc_count=2, no_registration_replay=True, unchanged_input_sha256=before_hash, ) diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-control.jsonl new file mode 100644 index 0000000000..1ed20c3722 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-control.jsonl @@ -0,0 +1,2 @@ +{"type": "openhcs_custom_function_registration_destination", "time": 1790723227.1595523} +{"type": "force_shutdown", "time": 1790723302.7396393} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-stderr.txt new file mode 100644 index 0000000000..a69ddf7c41 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-stderr.txt @@ -0,0 +1,20 @@ +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15992 (control: 16992) +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign.log +2026-09-29 19:06:53,772 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:54,460 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15992 (PUB), control tcp://127.0.0.1:16992 +2026-09-29 19:06:54,460 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:06:54,460 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:06:54,460 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:06:54,633 - zmqruntime.server - INFO - Server ready +2026-09-29 19:08:22,753 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:08:22,753 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:08:22,763 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:08:22,763 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:08:22,763 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/journey.json b/tests/runtime_diagnostics/registration_live_20260929_attempt01/journey.json new file mode 100644 index 0000000000..f60bfdc7c4 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/journey.json @@ -0,0 +1,921 @@ +{ + "accepted": false, + "source_sha": "81e636ad43566deb4a46392b9474dad40da16853", + "imports": { + "openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", + "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py" + }, + "submodules": [ + " b0004daf3223326ac78689ba7b2e68abc18ae298 external/ObjectState (v1.1.8)", + " 1209068f5fc9e4f7025a24ada8a73ef30dd1ddd7 external/PolyStore (v0.2.19-11-g1209068)", + " fddd9857bcd21468d20bc8547a769e0c6c9f1906 external/arraybridge (v0.3.4)", + " ca0a87e873f929b311a87a4d60cd3bfba315dbcf external/metaclass-registry (v0.2.1)", + " 792b03413331fdf107e369c924c257a2f0b16126 external/pycodify (v0.1.3)", + " a51c3d965f8ffdefe95a2b4b37347a85341d483a external/pyqt-reactive (v0.3.24)", + " 4e8f8bde5f750d9a50eacac80acd3806123addd8 external/python-introspect (v0.1.14-1-g4e8f8bd)", + " 1d3b32f4fcead23d5d2079c975eb2ae7646a1478 external/zmqruntime (v0.2.24)" + ], + "resources": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.6, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "driver_pid": 1779538, + "source_live_not_installed": true, + "calls": [ + { + "tool": "openhcs_health_check", + "arguments": {}, + "started_utc": 1790723224.8823767, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_health_check", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "status": "ok", + "started_at_unix": 1790723223.8288894, + "service": "openhcs.mcp", + "openhcs_version": "0.8.6", + "packaged_resources_ready": true, + "packaged_resource_count": 56, + "missing_packaged_resource_paths": [], + "server_process_id": 1781137, + "server_source_path": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/mcp/server.py", + "server_import_mtime_ns": 1790721541146819947, + "server_current_mtime_ns": 1790721541146819947, + "server_source_changed_since_import": false, + "stale_source_paths": [], + "recovery_reason": "current", + "installation_pointer_path": null, + "installation_pointer_changed_since_import": false, + "installation_pointer_available": null, + "restart_required": false, + "restart_command": [], + "restart_command_is_stable": false, + "reconnect_required": false, + "reconnect_owner": null, + "retry_after_reconnect": false, + "automatic_recovery_on_reconnect": false, + "restart_hint": null + } + ] + } + ] + }, + "elapsed_seconds": 0.01049570000031963 + }, + { + "tool": "openhcs_get_authoring_context", + "arguments": { + "kind": "first_use" + }, + "started_utc": 1790723224.9553807, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_authoring_context", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "kind": "first_use", + "content": "=== CHOOSE AN OPENHCS WORKFLOW ===\nUse this context when you are starting or resuming work and need to choose the OpenHCS state owner or next workflow.\n\n=== CORE MODEL IN ONE PASS ===\n- OpenHCS is a typed compiler/runtime for high-content microscopy. It projects microscope data into a storage-independent virtual workspace, compiles PipelineConfig plus ordered FunctionStep declarations, resolves multidimensional grouping and artifact contracts, executes through typed runtime values, and validates results through plate and viewer evidence.\n- Choose the state owner before choosing file formats or tools. If the user asks to open, show, continue, or edit work in the desktop, the UI-owned route always takes precedence over folder inspection or a headless session.\n\n=== CHOOSE ONE TASK ROUTE ===\nRequest exactly one matching context with openhcs_get_authoring_context; deepen into another only when the workflow reaches that boundary. Reuse this router after a handoff or a change of task; first_use is not a one-time setup step.\n- kind=\"ui_visible_workflow\" \u2014 Work in the running desktop UI: the user asks to open, show, continue, or edit work in the visible OpenHCS desktop.\n- kind=\"domain_expert_assisted_setup\" \u2014 Set up a workflow from biological intent: a domain expert describes the experiment and expected result but wants the agent to handle OpenHCS mechanics.\n- kind=\"folder_onboarding\" \u2014 Inspect and onboard image data: the task starts from a local plate, image folder, or rich image container and does not require visible UI ownership.\n- kind=\"pipeline\" \u2014 Author or revise a typed pipeline: the sources are understood and the next task is choosing functions, axes, grouping, configuration, or artifact contracts.\n- kind=\"image_analysis_workflow\" \u2014 Design and validate an image-analysis workflow: an analysis needs multisite assembly, registration, normalisation, mosaic quality control, segmentation review, or image-result provenance.\n- kind=\"custom_function\" \u2014 Author a custom processing function: the required operation is not already in the function registry or needs a typed custom artifact contract.\n- kind=\"headless_execution\" \u2014 Compile and execute headlessly: a reviewed pipeline should run without becoming selected or editable in the desktop UI.\n- kind=\"debugging\" \u2014 Diagnose compiled and runtime behavior: a compile, execution, step output, runtime value, or artifact must be inspected at its owning boundary.\n- kind=\"viewer_review\" \u2014 Validate results in a managed viewer: execution produced images, labels, or ROIs that need bounded visual and structured validation.\n- kind=\"objectstate_editing\" \u2014 Inspect or edit typed UI state: one exact UI-backed field, inheritance value, snapshot, or branch must be inspected or changed.\n- kind=\"cellprofiler_translation\" \u2014 Translate or validate a CellProfiler workflow: the intent begins with a .cppipe, CellProfiler module vocabulary, or an exact parity claim.\n\n=== SAFE FIRST ACTION ===\n- Start read-only. Query the active surface through openhcs_search_capabilities with task-relevant workflow, target, or text filters; its registry-owned metadata is the authority for what this server exposes. Use openhcs_list_capabilities only when the complete selected surface is required.\n- Guides describe workflows across capability profiles. Confirm a named tool is exposed before calling it. Desktop actions are discovered separately through openhcs_ui_list_actions; an empty capability search does not establish that a running window lacks the action.\n- Before operating a live desktop workflow, call openhcs_ui_list_windows and resolve visible modal top-level windows first. Inspect each dialog through openhcs_ui_get_widget_tree and invoke its declared button through openhcs_ui_invoke_widget_action; acknowledge completed operations, cancel unsolicited update/restart prompts, and never use desktop-coordinate automation or leave a blocking dialog hidden behind the analysis viewer.\n- Request the matching task context before mutation. Do not read every knowledge document, enumerate every function, invent config fields, or load full-resolution image data up front.\n- When continuing an analysis, read and reuse its existing pipeline and configuration. Adapt only the required source/layout choices, preserve earlier outputs in a separate destination, and record unavailable acquisitions as missing rather than zero measurements. Validate a representative bounded run before expanding.\n- If returned guidance is truncated, retrieve the same context with a sufficient max_chars before acting on an incomplete workflow.\n- Before any write or execution, show the exact target and intended change, refresh revision/request tokens, obtain approval, validate, and compile before running.\n\n=== DEEPEN ONLY WHEN NEEDED ===\nRetrieve one relevant source-backed target with openhcs_get_knowledge_document; use its returned section ids to narrow further instead of loading every document.\n- openhcs_architecture_quick_start \u2014 OpenHCS architecture quick start: Front-door OpenHCS architecture and MCP route covering the public PipelineConfig plus FunctionStep model, ObjectState resolution, function patterns, axes and grouping, typed artifacts, CellProfiler translation, generic source bindings, compilation, runtime values, examples, and safe operation." + } + ] + } + ] + }, + "elapsed_seconds": 1.3423105829861015 + }, + { + "tool": "openhcs_get_authoring_context", + "arguments": { + "kind": "custom_function" + }, + "started_utc": 1790723226.3642004, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_authoring_context", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "kind": "custom_function", + "content": "=== AUTHOR A CUSTOM PROCESSING FUNCTION ===\nUse this context when the required operation is not already in the function registry or needs a typed custom artifact contract.\n\n=== CUSTOM FUNCTIONS AND RUNTIME OUTPUTS ===\n- Custom functions are registry functions used inside FunctionStep.func patterns: callable, (callable, kwargs), list chains, or component/group keyed patterns.\n- A reviewed custom function becomes an ordinary registry-described callable; it does not create a separate pipeline mechanism. If openhcs_search_functions has no contract-compatible implementation for an intended operation, use this route, register the callable, then describe its reflected contract before composing it.\n- CLAHE, median denoising, background subtraction, and flat-field correction are example preprocessing intents, not a promised built-in catalog. Implement only the missing operation, keep it backend-agnostic at the pipeline boundary, and let its memory decorator, ProcessingContract, and artifact declarations state compatibility.\n- Dict patterns are routed by processing_config.group_by; list and single-callable patterns apply the same callable chain to each grouped 3D array.\n- processing_config.variable_components are the axes stacked into each callable input array; group_by groups the remaining 3D arrays and only selects callable branches for dict patterns.\n- Memory decorators such as @numpy define runtime memory conversion; FunctionStep processing_config defines grouping, axes, and input source.\n- Declare outputs with typed ArtifactSpec values. The decorator owns their input/output role. MeasurementsArtifactType receives schema-bearing ColumnarRows; ObjectLabelsArtifactType receives complete integer label arrays and may use segmentation_mask_rois for materialization.\n- Path-bearing topology belongs in one SpatialGraphArtifactType output whose SpatialGraph nodes, edges, and scalar features are authoritative. A directed morphology forest can materialize through SWCOptions while SpatialGraphROIOptions projects those same paths and features to Napari; do not reduce graph semantics to a mask or rebuild them in viewer code.\n- Artifact graph planning owns output names, sidecar results, materialization intent, and downstream artifact inputs; do not encode those as ad hoc files or hidden globals. Request the viewer-review context only after execution produces artifacts to inspect.\n\n=== CUSTOM FUNCTION RULES ===\n- Decorate exactly one callable with a memory decorator such as @numpy.\n- The first image-like argument is supplied by OpenHCS at runtime; do not pass it as a FunctionStep kwarg.\n- Prefer concrete typed parameters with serializable defaults.\n- Preserve dtype unless intentionally changing representation.\n- Declare measurements as MeasurementsArtifactType and return ColumnarRows; declare segmentation as ObjectLabelsArtifactType and return the complete integer label array.\n- Declare path topology as SpatialGraphArtifactType and return one SpatialGraph; use SWCOptions and SpatialGraphROIOptions as projections of that same value rather than independent graph and ROI computations.\n- Do not close over GUI, filesystem, or live viewer state.\n\n=== MINIMAL NUMPY CUSTOM FUNCTION ===\n```python\nfrom openhcs.core.memory import numpy\nimport numpy as np\n\n@numpy\ndef robust_clip(image, low_percentile: float = 1.0, high_percentile: float = 99.0):\n \"\"\"Clip intensities to robust percentiles while preserving dtype.\"\"\"\n array = np.asarray(image)\n if array.size == 0:\n return image\n low = np.percentile(array, low_percentile)\n high = np.percentile(array, high_percentile)\n if high <= low:\n return image\n return np.clip(array, low, high).astype(array.dtype, copy=False)\n```\n\n=== REGISTRATION WORKFLOW ===\n- Read the targeted custom-function and artifact-contract knowledge documents below before inventing decorators or payload types.\n- Register reviewed source through openhcs_register_custom_function; that capability delegates to the nominal CustomFunctionManager and owns persistence and registry refresh.\n- Search with openhcs_search_functions, describe the returned function_id with openhcs_describe_function, then request the pipeline context to add the function and validate its compiled artifact plan.\n\n=== DEEPEN ONLY WHEN NEEDED ===\nRetrieve one relevant source-backed target with openhcs_get_knowledge_document; use its returned section ids to narrow further instead of loading every document.\n- openhcs_custom_functions \u2014 OpenHCS custom functions: How to add custom processing functions, memory decorators, validation, GPU custom functions, registry integration, and GUI availability.\n- openhcs_custom_function_management \u2014 OpenHCS custom function management: CustomFunctionManager lifecycle, atomic updates, module injection, reload signals, and persisted custom-function registry behavior.\n- openhcs_artifact_contract_system \u2014 OpenHCS artifact contract system: Canonical callable/module artifact contracts, graph planning, exact satisfaction, runtime observations, and materialization.\n- openhcs_custom_function_workflow \u2014 Add a missing analysis operation as a typed custom function: Add a missing operation through typed custom-function owners: explicit endpoint/name/store admission before dispatch, controlled-launch native destination proof, postdispatch uncertainty without replay, stable imports, compilation and bounded outputs; separate technical reuse from biological acceptance." + } + ] + } + ] + }, + "elapsed_seconds": 0.14661152701592073 + }, + { + "tool": "openhcs_search_capabilities", + "arguments": { + "query": "custom function", + "limit": 5 + }, + "started_utc": 1790723226.574141, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_search_capabilities", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "surface_profile": "full", + "query": { + "text": null, + "kind": null, + "workflow_group": null, + "workflow_stage": null, + "target_context": null, + "visibility": null, + "role": null, + "has_side_effects": null, + "side_effect_contains": null, + "offset": 0, + "limit": 5 + }, + "matched_count": 98, + "returned_count": 5, + "next_offset": 5, + "capabilities": [ + { + "name": "openhcs://capabilities", + "kind": "resource", + "title": "OpenHCS agent capability registry", + "description": "Lists the resources, tools, side effects, and extras exposed by this server.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": null, + "output_type": "AgentCapabilityRegistry" + }, + { + "name": "openhcs_sweep_viewer_endpoints", + "kind": "tool", + "title": "Sweep viewer endpoints", + "description": "Sweeps the local OpenHCS IPC directory for live viewer endpoints, probes each through the viewer-window state authority, and classifies every live viewer as owned or foreign relative to this process. Dead sockets are filtered by a bind probe; non-viewer endpoint pairs (execution, UI bridge) are excluded.", + "workflow_group": "discovery", + "workflow_stage": "diagnostic", + "target_context": "server", + "visibility": "beginner", + "role": "diagnostic", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [ + "viewer_endpoint_inventory" + ], + "security_requirements": [], + "input_type": null, + "output_type": "ViewerEndpointDiscoveryResult" + }, + { + "name": "openhcs_health_check", + "kind": "tool", + "title": "Health check", + "description": "Reports OpenHCS MCP health, installed OpenHCS version, packaged-resource readiness, server process identity, source freshness, installation-generation freshness, and the client-owned reconnect contract for stale-process diagnostics.", + "workflow_group": "discovery", + "workflow_stage": "diagnostic", + "target_context": "server", + "visibility": "beginner", + "role": "diagnostic", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [ + "installed_openhcs_version", + "packaged_resource_readiness", + "packaged_resource_paths", + "mcp_process_identity", + "mcp_source_freshness", + "mcp_installation_generation" + ], + "security_requirements": [], + "input_type": null, + "output_type": "McpServerHealthResult" + }, + { + "name": "openhcs_list_capabilities", + "kind": "tool", + "title": "List capabilities", + "description": "Returns the canonical agent capability registry.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": null, + "output_type": "AgentCapabilityRegistry" + }, + { + "name": "openhcs_search_capabilities", + "kind": "tool", + "title": "Search capabilities", + "description": "Returns a bounded task-specific projection of the selected canonical capability registry. Filter by registry-owned workflow group, stage, target context, visibility, role, side effects, or free text before choosing tools.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": "AgentCapabilitySearchRequest", + "output_type": "AgentCapabilitySearchResult" + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.010072286997456104 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n" + }, + "started_utc": 1790723226.6511168, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Custom function registration requires an explicit port.", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "ValueError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.008670338982483372 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign", + "port": 15991, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723226.7264938, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.009579994017258286 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/ancestor-escape", + "port": 15991, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723226.8008244, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.013808730989694595 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data/openhcs/custom_functions", + "port": 15991, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723226.8834674, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign/registration_live_probe.py. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.008715464995475486 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data/openhcs/custom_functions", + "port": 15992, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723226.9602127, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Selected endpoint owns custom storage /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign/data/openhcs/custom_functions, source /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign/data/openhcs/custom_functions/registration_live_probe.py, not requested /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data/openhcs/custom_functions/registration_live_probe.py; no source was dispatched.", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "ValueError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.20088870599283837 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data/openhcs/custom_functions", + "port": 15993, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723227.2370539, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "{'status': 'error', 'type': 'error', 'message': 'Unsupported destination proof fixture'}", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "RuntimeError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.14866862702183425 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data/openhcs/custom_functions", + "port": 15991, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790723227.4554276, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [ + { + "code": "mcp_transport_failed", + "phase": "call_tool", + "exception_type": "TimeoutError", + "message": "", + "causes": [ + { + "exception_type": "TimeoutError", + "message": "" + } + ], + "hint": "The fresh OpenHCS MCP subprocess did not complete the requested stdio exchange. The dev client captures a bounded server stderr tail on transport failures so startup tracebacks remain visible without noisy successful calls.", + "server_stderr_tail": null + } + ], + "results": [] + }, + "elapsed_seconds": 10.011426267010393 + } + ], + "processes": [ + { + "role": "owned", + "port": 15991, + "identity": { + "pid": 1779678, + "create_time": 1790723205.17 + } + }, + { + "role": "foreign", + "port": 15992, + "identity": { + "pid": 1780013, + "create_time": 1790723209.4 + } + }, + { + "role": "unsupported", + "port": 15993, + "identity": { + "pid": 1780246, + "create_time": 1790723213.82 + } + } + ], + "original033": "preserved; not replayed", + "shared7777": "never contacted", + "uncertain_dispatch": null, + "scratch": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01", + "resource_observations": [ + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3328 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.0, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.1 GiB" + ], + "swap_used_gib": 11.1 + }, + "scratch_bytes": 3215 + } + ], + "mcp_pid": 1781137, + "mcp_launch_environment": { + "HOME": "/home/ts", + "LOGNAME": "ts", + "PATH": "/home/ts/.codex/packages/standalone/releases/0.159.1-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0ZH6jHa:/home/ts/.codex/tmp/arg0/codex-arg004m4pr:/home/ts/.codex/packages/standalone/releases/0.159.0-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0ChXg03:/home/ts/.codex/packages/standalone/releases/0.158.0-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0a3jKy1:/home/ts/.codex/packages/standalone/releases/0.157.1-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0fLf3ul:/usr/lib/openai-codex/codex-path:/home/ts/.pyenv/bin:/home/ts/bin:/usr/local/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/home/ts/.pyenv/bin:/home/ts/bin:/usr/local/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/opt/android-sdk/emulator:/opt/android-ndk:/opt/android-sdk/build-tools/30.0.3:/opt/cuda/bin:/home/ts/.local/share/flatpak/exports/bin:/var/lib/flatpak/exports/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/.dotfiles/revsl.sh:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/.dotfiles/revsl.sh:/opt/android-sdk/emulator:/opt/android-ndk", + "SHELL": "/bin/zsh", + "TERM": "dumb", + "USER": "ts", + "QT_QPA_PLATFORM": "offscreen", + "DISPLAY": ":0", + "XAUTHORITY": "/home/ts/.Xauthority", + "XDG_RUNTIME_DIR": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/runtime", + "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus", + "XDG_SESSION_TYPE": "x11", + "XDG_DATA_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/data", + "XDG_CACHE_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/cache", + "XDG_CONFIG_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/config", + "PYTHONPATH": "/home/ts/wt/openhcs-custom-function-admission-20260929:/home/ts/wt/openhcs-custom-function-admission-20260929/external/ObjectState/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/arraybridge/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/metaclass-registry/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pycodify/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pyqt-reactive/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/python-introspect/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/zmqruntime/src", + "OPENHCS_AGENT_READ_ROOTS": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01:/home/ts/wt/openhcs-custom-function-admission-20260929/packaging", + "OPENHCS_AGENT_WRITE_ROOTS": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned", + "OPENHCS_UI_CONFIG_CACHE_FILE": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/ui_config.config", + "OPENHCS_CPU_ONLY": "true", + "OPENHCS_HEADLESS": "true", + "NUMBA_CACHE_DIR": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/owned/numba", + "OPENHCS_USE_THREADING": "true", + "POLYSTORE_IMAGEJ_CACHE_ROOT": "/home/ts/.cache/polystore/imagej", + "POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD": "false", + "OMP_NUM_THREADS": "1", + "OPENBLAS_NUM_THREADS": "1", + "MKL_NUM_THREADS": "1", + "NUMEXPR_NUM_THREADS": "1", + "BLIS_NUM_THREADS": "1", + "NUMBA_NUM_THREADS": "1" + }, + "protected_sentinel": { + "path": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt01/foreign/registration_live_probe.py", + "sha256_before_after": "748cc865eb250527979464908234f357d1ddf6eceecc50057eed58df7c911b79", + "unchanged": true + }, + "failure": { + "type": "RuntimeError", + "message": "Uncertain transport outcome for openhcs_register_custom_function; do not replay.", + "traceback": "Traceback (most recent call last):\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/tests/diagnostics/check_custom_registration_live.py\", line 513, in run\n registration = register()\n ^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/tests/diagnostics/check_custom_registration_live.py\", line 371, in register\n return call(\n ^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/tests/diagnostics/check_custom_registration_live.py\", line 347, in call\n raise RuntimeError(\nRuntimeError: Uncertain transport outcome for openhcs_register_custom_function; do not replay.\n" + }, + "same_handle_observations": [ + { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_search_functions", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_search_functions", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Function registry preparation process failed: main()\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/runtime/zmq_execution_server_launcher.py\", line 152, in main\n capability_preparer()\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/runtime/function_catalog_preparation.py\", line 32, in prepare_persistent_catalog\n RegistryService.prepare_in_current_process()\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/lib_registry/registry_service.py\", line 113, in prepare_in_current_process\n cls._metadata_cache = cls._metadata_from_instances(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/lib_registry/registry_service.py\", line 191, in _metadata_from_instances\n functions = registry_instance.load_or_discover_functions()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/lib_registry/openhcs_registry.py\", line 410, in load_or_discover_functions\n functions = super().load_or_discover_functions()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/lib_registry/unified_registry.py\", line 1939, in load_or_discover_functions\n functions = self.discover_functions()\n ^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/lib_registry/openhcs_registry.py\", line 464, in discover_functions\n module_name: module.openhcs_registry_functions()\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/__init__.py\", line 63, in openhcs_registry_functions\n return tuple(\n ^^^^^^\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/__init__.py\", line 64, in \n module_type.require_callable(function_name)\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/interop/cellprofiler/module_declarations.py\", line 569, in require_callable\n implementation_module = importlib.import_module(cls.__module__)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/home/ts/.pyenv/versions/3.12.3/lib/python3.12/importlib/__init__.py\", line 90, in import_module\n return _bootstrap._gcd_import(name[level:], package, level)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"\", line 1387, in _gcd_import\n File \"\", line 1360, in _find_and_load\n File \"\", line 1331, in _find_and_load_unlocked\n File \"\", line 935, in _load_unlocked\n File \"\", line 995, in exec_module\n File \"\", line 488, in _call_with_frames_removed\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/intensity.py\", line 347, in \n from openhcs.processing.backends.cellprofiler.shape import (\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/shape.py\", line 48, in \n from openhcs.processing.backends.cellprofiler.zernike import (\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/zernike.py\", line 62, in \n from openhcs.processing.backends.cellprofiler.granularity import (\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/granularity.py\", line 94, in \n from openhcs.processing.backends.cellprofiler._granularity_reconstruct import (\nModuleNotFoundError: No module named 'openhcs.processing.backends.cellprofiler._granularity_reconstruct'", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "RuntimeError", + "path": null + } + ] + } + ] + } + ] + } + ], + "operator_disposition": "Close only diagnostic owned processes; do not replay any recorded call.", + "client_closed": true, + "closed_servers": [ + { + "role": "unsupported", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + }, + { + "role": "foreign", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + }, + { + "role": "owned", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + } + ], + "runtime_terminal": true +} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/mcp-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt01/mcp-stderr.txt new file mode 100644 index 0000000000..ca517f933b --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/mcp-stderr.txt @@ -0,0 +1 @@ +MCP progress: progress=0.0 message='Search processing functions: started' diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-control.jsonl new file mode 100644 index 0000000000..912c0fdc5b --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-control.jsonl @@ -0,0 +1,140 @@ +{"type": "openhcs_custom_function_registration_destination", "time": 1790723227.5803049} +{"type": "openhcs_custom_function_register", "time": 1790723227.5914738} +{"type": "openhcs_custom_function_register", "time": 1790723227.700273} +{"type": "openhcs_custom_function_register", "time": 1790723227.828733} +{"type": "openhcs_custom_function_register", "time": 1790723227.9353049} +{"type": "openhcs_custom_function_register", "time": 1790723228.0378568} +{"type": "openhcs_custom_function_register", "time": 1790723228.1401956} +{"type": "openhcs_custom_function_register", "time": 1790723228.2426481} +{"type": "openhcs_custom_function_register", "time": 1790723228.3451438} +{"type": "openhcs_custom_function_register", "time": 1790723228.4476557} +{"type": "openhcs_custom_function_register", "time": 1790723228.549765} +{"type": "openhcs_custom_function_register", "time": 1790723228.6522295} +{"type": "openhcs_custom_function_register", "time": 1790723228.7544117} +{"type": "openhcs_custom_function_register", "time": 1790723228.856656} +{"type": "openhcs_custom_function_register", "time": 1790723228.9588645} +{"type": "openhcs_custom_function_register", "time": 1790723229.0608008} +{"type": "openhcs_custom_function_register", "time": 1790723229.1632671} +{"type": "openhcs_custom_function_register", "time": 1790723229.2654696} +{"type": "openhcs_custom_function_register", "time": 1790723229.3676624} +{"type": "openhcs_custom_function_register", "time": 1790723229.4697216} +{"type": "openhcs_custom_function_register", "time": 1790723229.5817015} +{"type": "openhcs_custom_function_register", "time": 1790723229.6836612} +{"type": "openhcs_custom_function_register", "time": 1790723229.7856302} +{"type": "openhcs_custom_function_register", "time": 1790723229.887767} +{"type": "openhcs_custom_function_register", "time": 1790723229.9900217} +{"type": "openhcs_custom_function_register", "time": 1790723230.0922165} +{"type": "openhcs_custom_function_register", "time": 1790723230.1944058} +{"type": "openhcs_custom_function_register", "time": 1790723230.2966282} +{"type": "openhcs_custom_function_register", "time": 1790723230.3986053} +{"type": "openhcs_custom_function_register", "time": 1790723230.5005667} +{"type": "openhcs_custom_function_register", "time": 1790723230.6025488} +{"type": "openhcs_custom_function_register", "time": 1790723230.7045736} +{"type": "openhcs_custom_function_register", "time": 1790723230.806687} +{"type": "openhcs_custom_function_register", "time": 1790723230.9085698} +{"type": "openhcs_custom_function_register", "time": 1790723231.010428} +{"type": "openhcs_custom_function_register", "time": 1790723231.1129386} +{"type": "openhcs_custom_function_register", "time": 1790723231.2153547} +{"type": "openhcs_custom_function_register", "time": 1790723231.3177602} +{"type": "openhcs_custom_function_register", "time": 1790723231.4200447} +{"type": "openhcs_custom_function_register", "time": 1790723231.5223575} +{"type": "openhcs_custom_function_register", "time": 1790723231.6241837} +{"type": "openhcs_custom_function_register", "time": 1790723231.7261627} +{"type": "openhcs_custom_function_register", "time": 1790723231.8282433} +{"type": "openhcs_custom_function_register", "time": 1790723231.930196} +{"type": "openhcs_custom_function_register", "time": 1790723232.0324206} +{"type": "openhcs_custom_function_register", "time": 1790723232.1347198} +{"type": "openhcs_custom_function_register", "time": 1790723232.237456} +{"type": "openhcs_custom_function_register", "time": 1790723232.3399465} +{"type": "openhcs_custom_function_register", "time": 1790723232.4419832} +{"type": "openhcs_custom_function_register", "time": 1790723232.543925} +{"type": "openhcs_custom_function_register", "time": 1790723232.6459892} +{"type": "openhcs_custom_function_register", "time": 1790723232.7476933} +{"type": "openhcs_custom_function_register", "time": 1790723232.8498456} +{"type": "openhcs_custom_function_register", "time": 1790723232.9519124} +{"type": "openhcs_custom_function_register", "time": 1790723233.0537715} +{"type": "openhcs_custom_function_register", "time": 1790723233.1554577} +{"type": "openhcs_custom_function_register", "time": 1790723233.2576563} +{"type": "openhcs_custom_function_register", "time": 1790723233.360189} +{"type": "openhcs_custom_function_register", "time": 1790723233.462424} +{"type": "openhcs_custom_function_register", "time": 1790723233.5646634} +{"type": "openhcs_custom_function_register", "time": 1790723233.6668832} +{"type": "openhcs_custom_function_register", "time": 1790723233.768853} +{"type": "openhcs_custom_function_register", "time": 1790723233.8706975} +{"type": "openhcs_custom_function_register", "time": 1790723233.9724643} +{"type": "openhcs_custom_function_register", "time": 1790723234.074736} +{"type": "openhcs_custom_function_register", "time": 1790723234.1768725} +{"type": "openhcs_custom_function_register", "time": 1790723234.2789953} +{"type": "openhcs_custom_function_register", "time": 1790723234.3810422} +{"type": "openhcs_custom_function_register", "time": 1790723234.4831567} +{"type": "openhcs_custom_function_register", "time": 1790723234.585275} +{"type": "openhcs_custom_function_register", "time": 1790723234.6872966} +{"type": "openhcs_custom_function_register", "time": 1790723234.7892656} +{"type": "openhcs_custom_function_register", "time": 1790723234.8912764} +{"type": "openhcs_custom_function_register", "time": 1790723234.9931672} +{"type": "openhcs_custom_function_register", "time": 1790723235.0950842} +{"type": "openhcs_custom_function_register", "time": 1790723235.1972477} +{"type": "openhcs_custom_function_register", "time": 1790723235.299546} +{"type": "openhcs_custom_function_register", "time": 1790723235.4017649} +{"type": "openhcs_custom_function_register", "time": 1790723235.5041285} +{"type": "openhcs_custom_function_register", "time": 1790723235.6065872} +{"type": "openhcs_custom_function_register", "time": 1790723235.7088366} +{"type": "openhcs_custom_function_register", "time": 1790723235.811391} +{"type": "openhcs_custom_function_register", "time": 1790723235.913663} +{"type": "openhcs_custom_function_register", "time": 1790723236.0158513} +{"type": "openhcs_custom_function_register", "time": 1790723236.1179729} +{"type": "openhcs_custom_function_register", "time": 1790723236.219965} +{"type": "openhcs_custom_function_register", "time": 1790723236.3220487} +{"type": "openhcs_custom_function_register", "time": 1790723236.4241557} +{"type": "openhcs_custom_function_register", "time": 1790723236.5263987} +{"type": "openhcs_custom_function_register", "time": 1790723236.6285448} +{"type": "openhcs_custom_function_register", "time": 1790723236.7309413} +{"type": "openhcs_custom_function_register", "time": 1790723236.8330412} +{"type": "openhcs_custom_function_register", "time": 1790723236.934902} +{"type": "openhcs_custom_function_register", "time": 1790723237.0371342} +{"type": "openhcs_custom_function_register", "time": 1790723237.1393614} +{"type": "openhcs_custom_function_register", "time": 1790723237.2413702} +{"type": "openhcs_custom_function_register", "time": 1790723237.3434076} +{"type": "openhcs_custom_function_register", "time": 1790723237.4452434} +{"type": "openhcs_custom_function_register", "time": 1790723237.5471497} +{"type": "openhcs_custom_function_register", "time": 1790723237.6492825} +{"type": "openhcs_custom_function_register", "time": 1790723237.7514925} +{"type": "openhcs_custom_function_register", "time": 1790723237.8538373} +{"type": "openhcs_custom_function_register", "time": 1790723237.9560845} +{"type": "openhcs_custom_function_register", "time": 1790723238.0585363} +{"type": "openhcs_custom_function_register", "time": 1790723238.160728} +{"type": "openhcs_custom_function_register", "time": 1790723238.2631292} +{"type": "openhcs_custom_function_register", "time": 1790723238.3654606} +{"type": "openhcs_custom_function_register", "time": 1790723238.4675934} +{"type": "openhcs_custom_function_register", "time": 1790723238.569538} +{"type": "openhcs_custom_function_register", "time": 1790723238.67167} +{"type": "openhcs_custom_function_register", "time": 1790723238.7739446} +{"type": "openhcs_custom_function_register", "time": 1790723238.8760655} +{"type": "openhcs_custom_function_register", "time": 1790723238.9782755} +{"type": "openhcs_custom_function_register", "time": 1790723239.0804794} +{"type": "openhcs_custom_function_register", "time": 1790723239.1827438} +{"type": "openhcs_custom_function_register", "time": 1790723239.28506} +{"type": "openhcs_custom_function_register", "time": 1790723239.387107} +{"type": "openhcs_custom_function_register", "time": 1790723239.4892416} +{"type": "openhcs_custom_function_register", "time": 1790723239.5914938} +{"type": "openhcs_custom_function_register", "time": 1790723239.6936722} +{"type": "openhcs_custom_function_register", "time": 1790723239.7959273} +{"type": "openhcs_custom_function_register", "time": 1790723239.898175} +{"type": "openhcs_custom_function_register", "time": 1790723240.0002937} +{"type": "openhcs_custom_function_register", "time": 1790723240.1026585} +{"type": "openhcs_custom_function_register", "time": 1790723240.2052553} +{"type": "openhcs_custom_function_register", "time": 1790723240.307623} +{"type": "openhcs_custom_function_register", "time": 1790723240.409817} +{"type": "openhcs_custom_function_register", "time": 1790723240.5119922} +{"type": "openhcs_custom_function_register", "time": 1790723240.6142662} +{"type": "openhcs_custom_function_register", "time": 1790723240.716416} +{"type": "openhcs_custom_function_register", "time": 1790723240.8185954} +{"type": "openhcs_custom_function_register", "time": 1790723240.9212189} +{"type": "openhcs_custom_function_register", "time": 1790723241.023556} +{"type": "openhcs_custom_function_register", "time": 1790723241.1259916} +{"type": "openhcs_custom_function_register", "time": 1790723241.2282233} +{"type": "openhcs_custom_function_register", "time": 1790723241.3303478} +{"type": "openhcs_custom_function_register", "time": 1790723241.4324486} +{"type": "openhcs_custom_function_register", "time": 1790723241.53434} +{"type": "openhcs_function_catalog_search", "time": 1790723275.7328885} +{"type": "force_shutdown", "time": 1790723303.2127094} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-stderr.txt new file mode 100644 index 0000000000..ed717c4a88 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-stderr.txt @@ -0,0 +1,25 @@ +2026-09-29 19:06:49,476 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15991 (control: 16991) +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt01/owned.log +2026-09-29 19:06:49,477 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:50,183 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15991 (PUB), control tcp://127.0.0.1:16991 +2026-09-29 19:06:50,183 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:06:50,183 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:06:50,183 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:06:50,214 - zmqruntime.server - INFO - Server ready +2026-09-29 19:07:07,592 - openhcs.processing.backends.lib_registry.registry_service - INFO - Registry metadata cache cleared +2026-09-29 19:07:07,592 - openhcs.processing.custom_functions.manager - INFO - Loaded 0 persisted custom function(s) +2026-09-29 19:07:07,599 - metaclass_registry.discovery - INFO - Discovered 5 registry classes for LibraryRegistryBase: ['CupyRegistry', 'OpenHCSRegistry', 'PyclesperantoRegistry', 'SkimageRegistry', 'RuntimeTestingRegistryBase'] +2026-09-29 19:07:07,609 - openhcs.processing.backends.lib_registry.registry_service - INFO - CPU-only registry discovery skipping CupyRegistry +2026-09-29 19:07:07,609 - openhcs.processing.backends.lib_registry.registry_service - INFO - CPU-only registry discovery skipping PyclesperantoRegistry +2026-09-29 19:08:23,224 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:08:23,224 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:08:23,235 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:08:23,235 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:08:23,235 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-control.jsonl new file mode 100644 index 0000000000..0fae0d0866 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-control.jsonl @@ -0,0 +1,2 @@ +{"type": "openhcs_custom_function_registration_destination", "time": 1790723227.3854418} +{"type": "force_shutdown", "time": 1790723302.3072264} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-stderr.txt new file mode 100644 index 0000000000..909da434a9 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-stderr.txt @@ -0,0 +1,20 @@ +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15993 (control: 16993) +2026-09-29 19:06:58,195 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:06:58,196 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:06:58,196 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:06:58,196 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported.log +2026-09-29 19:06:58,196 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:06:58,883 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15993 (PUB), control tcp://127.0.0.1:16993 +2026-09-29 19:06:58,883 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:06:58,883 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:06:58,883 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:06:59,056 - zmqruntime.server - INFO - Server ready +2026-09-29 19:08:22,324 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:08:22,325 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:08:22,335 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:08:22,335 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:08:22,336 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/runtime_diagnostics/registration_native_build_20260929/build.log b/tests/runtime_diagnostics/registration_native_build_20260929/build.log new file mode 100644 index 0000000000..72e05ece27 --- /dev/null +++ b/tests/runtime_diagnostics/registration_native_build_20260929/build.log @@ -0,0 +1,5 @@ +running build_ext +building 'openhcs.processing.backends.cellprofiler._granularity_reconstruct' extension +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -fPIC -DPy_LIMITED_API=0x030B0000 -I/home/ts/code/projects/openhcs/.venv/include -I/home/ts/.pyenv/versions/3.12.3/include/python3.12 -c openhcs/processing/backends/cellprofiler/_granularity_reconstruct.cpp -o /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/objects/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.o -O3 +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -shared /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/objects/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.o -L/home/ts/.pyenv/versions/3.12.3/lib -o /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/lib/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so +copying /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/lib/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so -> openhcs/processing/backends/cellprofiler diff --git a/tests/runtime_diagnostics/registration_native_build_20260929/import.json b/tests/runtime_diagnostics/registration_native_build_20260929/import.json new file mode 100644 index 0000000000..de6ed4c7f5 --- /dev/null +++ b/tests/runtime_diagnostics/registration_native_build_20260929/import.json @@ -0,0 +1,9 @@ +{ + "extension": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so", + "bytes": 45440, + "sha256": "f0627903f263f701fc50736a934ee1b727ddcf4cbd97375f9933cf1c9182c091", + "cpp_sha256": "8aa0257600f694648979079ab824cfa706756e40418ae4d5362721f2d091b995", + "python": "/home/ts/code/projects/openhcs/.venv/bin/python", + "build": "setup.py build_ext --inplace --parallel 1 with owned build-temp/build-lib", + "scope": "source-tree build/import; installed225 already has its own compiled extension; no install, dependency download or registry bypass" +} diff --git a/tests/runtime_diagnostics/registration_native_build_20260929/resources.txt b/tests/runtime_diagnostics/registration_native_build_20260929/resources.txt new file mode 100644 index 0000000000..a966d5f270 --- /dev/null +++ b/tests/runtime_diagnostics/registration_native_build_20260929/resources.txt @@ -0,0 +1,23 @@ + Command being timed: "env OPENHCS_CPU_ONLY=true PYTHONDONTWRITEBYTECODE=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPATH=/home/ts/wt/openhcs-custom-function-admission-20260929:/home/ts/wt/openhcs-custom-function-admission-20260929/external/ObjectState/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/arraybridge/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/metaclass-registry/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pycodify/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pyqt-reactive/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/python-introspect/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/zmqruntime/src POLYSTORE_IMAGEJ_CACHE_ROOT=/home/ts/.cache/polystore/imagej POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD=false OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 /home/ts/code/projects/openhcs/.venv/bin/python -B setup.py build_ext --inplace --parallel 1 --build-temp /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/objects --build-lib /home/ts/.cache/agent-scratch/openhcs-registration-native-build-20260929/lib" + User time (seconds): 1.33 + System time (seconds): 0.13 + Percent of CPU this job got: 98% + Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.48 + Average shared text size (kbytes): 0 + Average unshared data size (kbytes): 0 + Average stack size (kbytes): 0 + Average total size (kbytes): 0 + Maximum resident set size (kbytes): 93004 + Average resident set size (kbytes): 0 + Major (requiring I/O) page faults: 0 + Minor (reclaiming a frame) page faults: 22710 + Voluntary context switches: 28 + Involuntary context switches: 142 + Swaps: 0 + File system inputs: 56 + File system outputs: 288 + Socket messages sent: 0 + Socket messages received: 0 + Signals delivered: 0 + Page size (bytes): 4096 + Exit status: 0 diff --git a/tests/runtime_diagnostics/registration_readiness_20260929/pytest.txt b/tests/runtime_diagnostics/registration_readiness_20260929/pytest.txt new file mode 100644 index 0000000000..925a4adb9d --- /dev/null +++ b/tests/runtime_diagnostics/registration_readiness_20260929/pytest.txt @@ -0,0 +1,16 @@ +openhcs=/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py +polystore=/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py +............................................... [100%] +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +47 passed, 25 deselected, 2 warnings in 7.32s diff --git a/tests/runtime_diagnostics/registration_readiness_20260929/receipt.json b/tests/runtime_diagnostics/registration_readiness_20260929/receipt.json new file mode 100644 index 0000000000..f851dbeba8 --- /dev/null +++ b/tests/runtime_diagnostics/registration_readiness_20260929/receipt.json @@ -0,0 +1,40 @@ +{ + "scope": "focused provider-free source checkpoint, not cold/live/installed acceptance", + "source_parent": "f5dbebb2798998646fcb85bfeff1185987d4a04e", + "polystore": "1209068f5fc9e4f7025a24ada8a73ef30dd1ddd7", + "python": "/home/ts/code/projects/openhcs/.venv/bin/python", + "source": "/home/ts/wt/openhcs-custom-function-admission-20260929", + "passed": 47, + "deselected": 25, + "pytest_seconds": 7.32, + "process_seconds": 8.13, + "peak_rss_kib": 305992, + "exit_code": 0, + "warnings": "two disabled async-plugin configuration options", + "pytest_arguments": [ + "--noconftest", "--import-mode=importlib", "-p", "no:cacheprovider", + "-o", "addopts=", "-q", "--tb=short", + "--basetemp=/home/ts/.cache/agent-scratch/openhcs-registration-readiness-20260929/pytest", + "tests/unit/agent/test_custom_registration_admission.py", + "tests/unit/test_function_catalog_zmq.py", + "-k", "registration or native_preparation or mcp_context or readiness or preparation_failure" + ], + "environment": { + "PYTHONPATH": "reviewed worktree plus all eight recorded submodule src directories", + "OPENHCS_CPU_ONLY": "true", + "PYTHONDONTWRITEBYTECODE": "1", + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "POLYSTORE_IMAGEJ_CACHE_ROOT": "/home/ts/.cache/polystore/imagej", + "POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD": "false", + "OMP_OPENBLAS_MKL_NUMEXPR_NUMBA_BLIS_THREADS": "1" + }, + "lock": "nonblocking shared validation.lock, released on test process exit", + "scratch_bytes_allocated": 335872, + "owned_disposable_fixtures_removed_after_receipts": true, + "handles_terminal": true, + "limitations": [ + "controlled pending future, not actual cold supervised kernel preparation", + "no MCP subprocess, JVM, GUI, install, downloads or scientific inputs", + "new heavy journey held for non-swap disk headroom release" + ] +} diff --git a/tests/runtime_diagnostics/registration_readiness_20260929/resources.txt b/tests/runtime_diagnostics/registration_readiness_20260929/resources.txt new file mode 100644 index 0000000000..d0e9e13a79 --- /dev/null +++ b/tests/runtime_diagnostics/registration_readiness_20260929/resources.txt @@ -0,0 +1 @@ +elapsed=8.13 max_rss_kib=305992 exit=0 diff --git a/tests/unit/agent/test_custom_registration_admission.py b/tests/unit/agent/test_custom_registration_admission.py index 7c5b9966b3..bb865ab725 100644 --- a/tests/unit/agent/test_custom_registration_admission.py +++ b/tests/unit/agent/test_custom_registration_admission.py @@ -9,6 +9,7 @@ import pytest from zmqruntime.config import TransportMode from zmqruntime.messages import ProcessIdentity +from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus from openhcs.agent.dto.common import SCHEMA_VERSION from openhcs.agent.dto.functions import ( @@ -16,6 +17,10 @@ CustomFunctionRegistrationDestinationRequest, CustomFunctionRegistrationRequest, CustomFunctionRegistrationResult, + FunctionCatalogNotReadyError, + FunctionCatalogPreparationHandle, + FunctionCatalogPreparationOutcome, + FunctionCatalogPreparationState, ) from openhcs.agent.path_policy import AgentPathPolicy, AgentPathPolicyError from openhcs.agent.services.endpoint_function_catalog_service import ( @@ -47,6 +52,17 @@ def policy(root): return AgentPathPolicy.with_roots(readable_roots=(root,), writable_roots=(root,)) +def preparation_state(handle, outcome=FunctionCatalogPreparationOutcome.READY): + return FunctionCatalogPreparationState( + schema_version=SCHEMA_VERSION, + handle=handle, + outcome=outcome, + progress=EndpointStartupStatus( + phase=EndpointStartupPhase.PREPARING_CAPABILITIES, message="Witness" + ), + ) + + def test_missing_explicit_route_rejects_before_client_creation(tmp_path): made = [] catalog = ZMQFunctionCatalogService( @@ -96,14 +112,16 @@ def test_exact_owned_route_and_store_precede_mutation(tmp_path, wrong_store, tim endpoints = [] class Client: - def custom_function_registration_destination(self, probe): + def custom_function_registration_destination( + self, probe, *, operation_deadline=None + ): assert probe.function_name == "boundary_probe" native = tmp_path / "foreign" if wrong_store else root return CustomFunctionRegistrationDestination( str(native), str(native / "boundary_probe.py") ) - def register_custom_function(self, admitted): + def register_custom_function(self, admitted, *, operation_deadline=None): mutations.append(admitted) assert admitted.admission_policy == policy(tmp_path) assert admitted.server_identity == ProcessIdentity.current() @@ -117,9 +135,12 @@ def register_custom_function(self, admitted): source_file_paths=(str(root / "boundary_probe.py"),), ) - def get_function_catalog(self, read_request): + def function_catalog_preparation( + self, read_request, *, operation_deadline=None + ): assert not hasattr(read_request, "source_code") assert not mutations + return preparation_state(read_request.handle) def disconnect(self): pass @@ -268,12 +289,14 @@ def test_wrong_owner_mutation_receipt_is_uncertain_not_replayed(tmp_path): mutations = [] class Client: - def custom_function_registration_destination(self, probe): + def custom_function_registration_destination( + self, probe, *, operation_deadline=None + ): return CustomFunctionRegistrationDestination( str(tmp_path), str(tmp_path / "boundary_probe.py") ) - def register_custom_function(self, admitted): + def register_custom_function(self, admitted, *, operation_deadline=None): mutations.append(admitted) return CustomFunctionRegistrationResult( schema_version=SCHEMA_VERSION, @@ -281,9 +304,12 @@ def register_custom_function(self, admitted): server_identity=replace(ProcessIdentity.current(), create_time=0), ) - def get_function_catalog(self, read_request): + def function_catalog_preparation( + self, read_request, *, operation_deadline=None + ): assert not hasattr(read_request, "source_code") assert not mutations + return preparation_state(read_request.handle) def disconnect(self): pass @@ -304,24 +330,33 @@ def disconnect(self): catalog.close() +@pytest.mark.parametrize( + "outcome", + tuple(state for state in FunctionCatalogPreparationOutcome if not state.ready), +) def test_readiness_failure_precedes_mutation_and_is_not_postdispatch_uncertainty( tmp_path, + outcome, ): observed = [] class Client: - def custom_function_registration_destination(self, probe): + def custom_function_registration_destination( + self, probe, *, operation_deadline=None + ): observed.append("destination") return CustomFunctionRegistrationDestination( str(tmp_path), str(tmp_path / "boundary_probe.py") ) - def get_function_catalog(self, read_request): + def function_catalog_preparation( + self, read_request, *, operation_deadline=None + ): assert not hasattr(read_request, "source_code") observed.append("read-only-preparation") - raise RuntimeError("native preparation failed") + return preparation_state(read_request.handle, outcome) - def register_custom_function(self, admitted): + def register_custom_function(self, admitted, *, operation_deadline=None): pytest.fail("Source must not be dispatched after readiness failure") def disconnect(self): @@ -333,7 +368,7 @@ def disconnect(self): path_policy=policy(tmp_path), ) try: - with pytest.raises(RuntimeError, match="native preparation failed") as failure: + with pytest.raises(FunctionCatalogNotReadyError) as failure: catalog.register_custom_function(request(tmp_path)) assert not isinstance(failure.value, CustomFunctionRegistrationUncertainError) assert observed == ["destination", "read-only-preparation"] @@ -494,3 +529,58 @@ async def invoke(): ) assert not made catalog.close() + + +def test_generated_mcp_preparation_tools_use_reflected_connection_and_handle(): + from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec + from openhcs.mcp.server import build_server + + observed = [] + handle = FunctionCatalogPreparationHandle( + ExecutionConnectionSpec(port=15993, transport_mode=TransportMode.TCP), + ProcessIdentity.current(), + ) + + class Catalog: + def start_catalog_preparation(self, connection): + observed.append(connection) + return preparation_state(handle, FunctionCatalogPreparationOutcome.PENDING) + + def catalog_preparation_status(self, current_handle): + observed.append(current_handle) + return preparation_state(current_handle) + + def cancel_catalog_preparation(self, current_handle): + observed.append(current_handle) + return preparation_state( + current_handle, FunctionCatalogPreparationOutcome.CANCELLED + ) + + async def invoke(): + built = build_server(SimpleNamespace(endpoint_function_catalog=Catalog())) + tools = {tool.name: tool for tool in await built.list_tools()} + assert {"port", "host", "transport_mode"} <= tools[ + "openhcs_start_function_catalog_preparation" + ].inputSchema["properties"].keys() + assert {"connection", "server_identity"} == tools[ + "openhcs_get_function_catalog_preparation_status" + ].inputSchema["properties"].keys() + started = await built.call_tool( + "openhcs_start_function_catalog_preparation", + {"port": 15993, "transport_mode": "tcp"}, + ) + from openhcs.serialization.json import to_jsonable + + args = to_jsonable(handle) + status = await built.call_tool( + "openhcs_get_function_catalog_preparation_status", args + ) + cancelled = await built.call_tool( + "openhcs_cancel_function_catalog_preparation", args + ) + for response in (started, status, cancelled): + content = response[0] if isinstance(response, tuple) else response.content + assert not json.loads(content[0].text).get("errors") + + asyncio.run(invoke()) + assert observed == [handle.connection, handle, handle] diff --git a/tests/unit/test_function_catalog_zmq.py b/tests/unit/test_function_catalog_zmq.py index 61e83c8c93..4c83ff5937 100644 --- a/tests/unit/test_function_catalog_zmq.py +++ b/tests/unit/test_function_catalog_zmq.py @@ -4,6 +4,7 @@ import inspect import textwrap import threading +import time from concurrent.futures import CancelledError, Future from dataclasses import replace @@ -11,8 +12,10 @@ from zmqruntime import OperationCancellation from zmqruntime.client import EndpointConnectionPolicy from zmqruntime.execution import ExecutionServer +from zmqruntime.messages import ProcessIdentity from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus +from openhcs.agent.dto.execution_connection import ExecutionConnectionSpec from openhcs.agent.dto.functions import ( CustomFunctionRegistrationControlResponse, CustomFunctionRegistrationDestinationControlResponse, @@ -24,7 +27,12 @@ FunctionCatalogControlRequestABC, FunctionCatalogControlResponse, FunctionCatalogEntry, + FunctionCatalogPreparationCancelRequest, FunctionCatalogPreparationControlResponse, + FunctionCatalogPreparationOutcome, + FunctionCatalogPreparationStartRequest, + FunctionCatalogPreparationStateControlResponse, + FunctionCatalogPreparationStatusRequest, FunctionDetail, FunctionDetailControlRequest, FunctionDetailControlResponse, @@ -38,6 +46,7 @@ from openhcs.agent.services.function_catalog_service import FunctionCatalogService from openhcs.core.callable_contract import CallableImportIdentity from openhcs.core.function_reference import ImportableFunctionReference +from openhcs.runtime.function_catalog_preparation import FunctionCatalogPreparation from openhcs.runtime.zmq_control import ( ZMQControlMessageRouter, ZMQControlRequestContext, @@ -95,6 +104,7 @@ def test_local_mcp_context_uses_persisted_desktop_execution_endpoint( context = create_agent_context() assert isinstance(context.function_catalog, ZMQFunctionCatalogService) + assert context.function_catalog is context.endpoint_function_catalog assert context.function_catalog._config_provider() == endpoint_config context.function_catalog.close() @@ -107,6 +117,94 @@ def test_hosted_mcp_context_keeps_self_contained_catalog() -> None: assert isinstance(context.function_catalog, FunctionCatalogService) +@pytest.mark.parametrize("construction", ("default_factory", "raw", "injected")) +def test_real_mcp_context_composes_typed_preparation_and_catalog_authority( + monkeypatch, construction +) -> None: + import asyncio + import json + + from openhcs.agent.services.endpoint_function_catalog_service import ( + EndpointFunctionCatalogServiceABC, + ZMQFunctionCatalogService, + ) + from openhcs.mcp.context import OpenHCSAgentContext, create_agent_context + from openhcs.mcp.server import build_server + from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG + from openhcs.serialization.json import to_jsonable + + connection = ExecutionConnectionSpec(port=22319) + endpoint = replace(OPENHCS_ZMQ_CONFIG, default_port=connection.port) + monkeypatch.setattr( + "openhcs.pyqt_gui.config.load_cached_ui_execution_endpoint_sync", + lambda: endpoint, + ) + preparation = FunctionCatalogPreparation(FunctionCatalogService()) + preparation._future = Future() + native = ZMQControlRequestContext( + compiled_artifacts={}, + function_catalog_preparation=preparation, + ) + observed = [] + + class Client: + def function_catalog_preparation(self, request, *, operation_deadline=None): + observed.append(request) + response = ZMQControlMessageRouter.handle( + FunctionCatalogControlPayload.from_request(request).to_dict(), + native, + ) + return FunctionCatalogPreparationStateControlResponse.from_control_response( + response + ).value + + def disconnect(self): + pass + + monkeypatch.setattr( + ZMQFunctionCatalogService, "_new_client", lambda self, config: Client() + ) + factories = { + "default_factory": create_agent_context, + "raw": OpenHCSAgentContext, + "injected": lambda: OpenHCSAgentContext( + endpoint_function_catalog=ZMQFunctionCatalogService(lambda: endpoint), + ), + } + context = factories[construction]() + assert isinstance( + context.endpoint_function_catalog, EndpointFunctionCatalogServiceABC + ) + assert context.function_catalog is context.endpoint_function_catalog + + async def invoke(): + built = build_server(context) + started = await built.call_tool( + "openhcs_start_function_catalog_preparation", + {"port": connection.port}, + ) + content = started[0] if isinstance(started, tuple) else started.content + payload = json.loads(content[0].text) + assert not payload["errors"] + assert payload["outcome"] == "pending" + arguments = to_jsonable(preparation.start(connection).handle) + for tool in ( + "openhcs_get_function_catalog_preparation_status", + "openhcs_cancel_function_catalog_preparation", + ): + response = await built.call_tool(tool, arguments) + content = response[0] if isinstance(response, tuple) else response.content + assert not json.loads(content[0].text)["errors"] + + try: + asyncio.run(invoke()) + assert len(observed) == 3 + assert preparation._thread is None + assert preparation._cancellation.requested() + finally: + context.endpoint_function_catalog.close() + + def test_gui_application_setup_does_not_initialize_execution_catalog() -> None: from openhcs.pyqt_gui.app import OpenHCSPyQtApp @@ -156,32 +254,19 @@ def _detail(entry: FunctionCatalogEntry | None = None) -> FunctionDetail: ) -class _StaticFunctionCatalogPreparation: - def __init__(self, future: Future[None]) -> None: - self.future = future - - def ensure_started(self) -> Future[None]: - return self.future - - def snapshot(self) -> EndpointStartupStatus: - return EndpointStartupStatus( - sequence=1, - phase=EndpointStartupPhase.PREPARING_CAPABILITIES, - message="Discovering functions", - timestamp=1.0, - ) - - def _context( preparation_future: Future[None] | None = None, ) -> ZMQControlRequestContext: ready = preparation_future or Future() if preparation_future is None: ready.set_result(None) + catalog = FunctionCatalogService() + preparation = FunctionCatalogPreparation(catalog) + preparation._future = ready return ZMQControlRequestContext( compiled_artifacts={}, - function_catalog=FunctionCatalogService(), - function_catalog_preparation=_StaticFunctionCatalogPreparation(ready), + function_catalog=catalog, + function_catalog_preparation=preparation, ) @@ -551,7 +636,9 @@ def test_zmq_router_registers_custom_source_through_catalog_owner( monkeypatch, ) -> None: request = CustomFunctionRegistrationRequest( - source_code="@numpy\ndef sample(image):\n return image\n" + source_code="@numpy\ndef sample(image):\n return image\n", + connection=ExecutionConnectionSpec(port=22319), + server_identity=ProcessIdentity.current(), ) result = CustomFunctionRegistrationResult( schema_version="openhcs.agent.v1", @@ -613,6 +700,102 @@ def reject_preparation(): assert not (tmp_path / "absent").exists() +def test_native_preparation_start_status_cancel_are_responsive_and_incarnation_owned(): + entered = threading.Event() + + class ControlledCatalog: + def prepare(self, *, status_callback, cancellation): + status_callback("Controlled preparation remains pending") + entered.set() + assert cancellation.wait(2) + raise CancelledError() + + preparation = FunctionCatalogPreparation(ControlledCatalog()) + context = ZMQControlRequestContext( + compiled_artifacts={}, function_catalog_preparation=preparation + ) + + def dispatch(request): + started = time.monotonic() + response = ZMQControlMessageRouter.handle( + FunctionCatalogControlPayload.from_request(request).to_dict(), context + ) + assert time.monotonic() - started < 0.5 + return FunctionCatalogPreparationStateControlResponse.from_control_response( + response + ).value + + try: + connection = ExecutionConnectionSpec(port=22319) + state = dispatch(FunctionCatalogPreparationStartRequest(connection)) + assert state.outcome is FunctionCatalogPreparationOutcome.PENDING + assert entered.wait(1) + future, thread = preparation._future, preparation._thread + assert ( + dispatch(FunctionCatalogPreparationStartRequest(connection)).handle + == state.handle + ) + assert preparation._future is future and preparation._thread is thread + assert ( + dispatch(FunctionCatalogPreparationStatusRequest(state.handle)).outcome + is FunctionCatalogPreparationOutcome.PENDING + ) + stale = replace( + state.handle, + server_identity=replace(state.handle.server_identity, create_time=0), + ) + with pytest.raises(RuntimeError, match="owner changed"): + preparation.cancel_preparation(stale) + assert not preparation._cancellation.requested() + cancelled = dispatch(FunctionCatalogPreparationCancelRequest(state.handle)) + assert cancelled.outcome in ( + FunctionCatalogPreparationOutcome.CANCELLING, + FunctionCatalogPreparationOutcome.CANCELLED, + ) + preparation.cancel_and_join() + assert ( + dispatch(FunctionCatalogPreparationStatusRequest(state.handle)).outcome + is FunctionCatalogPreparationOutcome.CANCELLED + ) + assert preparation._future is future and not thread.is_alive() + finally: + preparation.cancel_and_join() + + +def test_native_registration_does_not_start_or_wait_for_cold_preparation(monkeypatch): + context = ZMQControlRequestContext( + compiled_artifacts={}, + function_catalog=FunctionCatalogService(), + function_catalog_preparation=FunctionCatalogPreparation( + FunctionCatalogService() + ), + ) + monkeypatch.setattr( + context.function_catalog_preparation, + "ensure_started", + lambda: pytest.fail("Mutation cannot initiate warmup"), + ) + monkeypatch.setattr( + context.function_catalog, + "register_custom_function", + lambda _request: pytest.fail("Cold mutation must reject before evaluation"), + ) + request = CustomFunctionRegistrationRequest( + source_code="never evaluated", + persist=False, + connection=ExecutionConnectionSpec(port=22319), + server_identity=ProcessIdentity.current(), + ) + response = ZMQControlMessageRouter.handle( + FunctionCatalogControlPayload.from_request(request).to_dict(), context + ) + assert ( + response["status"] == "error" + and "No source was dispatched" in response["error"] + ) + assert context.function_catalog_preparation._future is None + + def test_zmq_router_delegates_search_to_catalog_owner(monkeypatch) -> None: catalog = _catalog() observed = [] From 4c6fbac0a0edb7805b36f66e9c5a56d7d2cd58f5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 19:50:22 -0400 Subject: [PATCH 10/13] Include stale preparation cancellation in bounded live diagnostic --- docs/plans/custom_registration_admission_20260929.md | 4 ++++ tests/diagnostics/check_custom_registration_live.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 0a5d67fe1d..0c5bc132e4 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -130,6 +130,10 @@ RPCs before READY and exactly two total** (initial positive plus controlled dela not merely one persistence marker. The complete journey is registration/discovery/ compile/execution of an8x8 plus-three synthetic function, escaping-path sentinels, foreign/unsupported predispatch denials and same-handle uncertain receipt reconciliation. +It also sends one stale-incarnation cancellation request while observing the +owned preparation, then retains that same valid handle until READY. Pending +valid cancellation/terminal cleanup is covered by the controlled source future +test; it is not claimed live from a cancellation attempted after readiness. --installed-entrypoint mode requires PYTHONPATH unset, cwd outside SOURCE, actual OpenHCS/PolyStore imports matching the reviewed installed source, and the ordinary diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index e937094d4d..9996f99bdf 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -546,6 +546,16 @@ def finish_job(ref): FunctionCatalogPreparationState, ) receipt["preparation_handle"] = to_jsonable(state.handle) + stale_handle = replace( + state.handle, + server_identity=replace(state.handle.server_identity, create_time=0), + ) + call( + "openhcs_cancel_function_catalog_preparation", + to_jsonable(stale_handle), + error_code="mcp_tool_failed", + ) + receipt["stale_preparation_cancel_rejected"] = True preparation_deadline = time.monotonic() + 100 if not state.outcome.ready: register(error_code="function_catalog_not_ready") From 161c453335d8dce0071df09d9e4a2ed6652cf2e9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 20:05:02 -0400 Subject: [PATCH 11/13] Require existing registration endpoints and retain cold preparation evidence --- .../custom_registration_admission_20260929.md | 44 +- openhcs/runtime/zmq_execution_client.py | 42 +- .../check_custom_registration_live.py | 17 +- .../pytest.txt | 16 + .../resources.txt | 1 + .../foreign-control.jsonl | 3 + .../foreign-stderr.txt | 20 + .../journey.json | 7730 +++++++++++++++++ .../mcp-stderr.txt | 0 .../owned-control.jsonl | 98 + .../owned-stderr.txt | 29 + .../unsupported-control.jsonl | 3 + .../unsupported-stderr.txt | 20 + tests/unit/test_function_catalog_zmq.py | 116 +- 14 files changed, 8104 insertions(+), 35 deletions(-) create mode 100644 tests/runtime_diagnostics/registration_attach_only_20260929/pytest.txt create mode 100644 tests/runtime_diagnostics/registration_attach_only_20260929/resources.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/journey.json create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/mcp-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-stderr.txt create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-control.jsonl create mode 100644 tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-stderr.txt diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 0c5bc132e4..85ae0f68d3 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -6,6 +6,19 @@ Source: /home/ts/wt/openhcs-custom-function-admission-20260929. This checkpoint follows normal main238 integration f5dbebb27 (main01d1a8c55c); recorded PolyStore1209068 is unchanged. No installed tree is edited. +Latest source follow-up after4c6fbac0a: destination proof and source dispatch +now use existing connect_existing, never attach-or-start/replacement. Each +reuses the existing operation/control deadline for attachment and the one +control exchange. Eight absent/unresponsive endpoint cases exercise the actual +attach-only method and assert zero start, kill and source/control sends; +two expired-deadline cases reject before attachment. Current focused shard: +**57 passed,25 deselected,7.22s**, process8.04s,peak309668KiB,exit0. +Receipt: tests/runtime_diagnostics/registration_attach_only_20260929/. +Full startup-lock contention boundedness is not claimed: the recorded dependency's +attach-only method has no operation_deadline parameter for that existing lock. +The control/send deadline still prevents delayed lock acquisition from later +dispatching source after budget expiry. No duplicate lock/lifecycle was added. + ## Working source Registration requires an explicit reflected ExecutionConnectionSpec port, also @@ -93,6 +106,23 @@ MCP and all three native handles are terminal; each server returned0, identities were verified and the lock released. Original H002033 and foreign saved side effect are preserved and never replayed. No science, shared7777 contact, GUI or JVM. +Attempt02 at exact4c6fbac0a is also preserved as accepted=false. Ordinary MCP +start returned0.020s;91 preparation-control observations stayed below0.027s. +Stale cancellation rejected; pre-ready native source-bearing register count +was exactly0. The diagnostic's redundant100s preparation sub-budget expired +while the future was pending; this was not a tool timeout or hung MCP. +Same-handle read-only observation subsequently returned READY: native progress +timestamps1790725945.1608434 to1790726055.660598 (~110.50s). The supervised +child was absent; MCP and all three native endpoints were closed by verified +owner identity, returncodes0, runtime_terminal=true, lock released. Source and +all original calls stayed frozen until closure. No mutation, compile or execution +occurred. Slow real cold warming remains a limitation, not an optimisation claim; +parent owns additional warmup diagnosis. +Receipt: tests/runtime_diagnostics/registration_live_20260929_attempt02/. +The next fresh journey removes redundant100s preparation/45s job subcaps, +retaining the existing single240s total journey gate and unchanged10s per-call +observation cap. Cold, compile and execution stage costs are recorded separately. + ## Focused architectural audit Scope: changed production declarations, callers and source tests; no full NRA scan @@ -104,6 +134,9 @@ or global proof. Actual catalog witnesses: - IMPL-13: start/status/cancel extends the one native future/cancellation/child. Repeated start coalesces the exact future/thread; failed readiness sends zero register RPCs. Mutation no longer borrows the discovery retry mechanism. + Registration and destination proof now consume the existing attach-only + lifecycle, not a caller-side replacement implementation. Eight absent/ + unresponsive cases and two expired-budget cases guard the route closure. - IMPL-12: Manager.source_path_for_name owns save/admission, named reads/deletes/ updates, require_source and rename destinations. Five interception cases prove those operations cannot bypass it. Directory enumeration reads real files and @@ -141,11 +174,12 @@ McpDevServerSpec. It is implemented but not live-proved. Source mode is explicit labelled source_live_not_installed. Parent owns final merge/install and installed acceptance; no frozen installation changes are authorised here. -At the latest worker guard, /home19.9GiB and RAM12.1GiB add a non-swap disk warning. -No new heavy runtime starts under that warning. The bounded47-case source shard -allocated328KiB fixture scratch; it is terminal and released the lock. Pending -dependency: verified non-swap headroom release/recheck, then finite cold source-live -journey and parent installed acceptance. No optional hosted CI wait. +Parent cleared disk headroom; latest actual guard /home20.2GiB,RAM13.1GiB warns +only about historical swap. Attempt02 and the57-case source follow-up are +terminal and released the lock. Next is the authorised finite cold source-live +journey on a fresh guard/nonblocking lock, then parent review/merge and +installation of this same frozen tree (no extra checkout). No optional hosted +CI wait or installed readiness claim. Owned disposable build objects136KiB, filename-test fixtures40KiB and current source-test fixtures328KiB were removed after retaining receipts. They are reproducible diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 348c3d6560..9d9e9e63e0 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -912,17 +912,20 @@ def register_custom_function( FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect( - operation_deadline=operation_deadline + deadline = operation_deadline or OperationDeadline.after_milliseconds( + self.config.control_timeout_ms, + operation="custom function registration", + ) + if not self.is_connected() and not self.connect_existing( + timeout=deadline.cap_seconds(1.0), ): - raise RuntimeError("Failed to connect to execution server") - payload = FunctionCatalogControlPayload.from_request(request).to_dict() - response = ( - self._send_control_request(payload) - if operation_deadline is None - else self._send_control_request( - payload, timeout_ms=operation_deadline.remaining_milliseconds() + raise RuntimeError( + "Custom registration requires an existing execution endpoint." ) + payload = FunctionCatalogControlPayload.from_request(request).to_dict() + response = self._send_control_request( + payload, + timeout_ms=deadline.remaining_milliseconds(), ) return CustomFunctionRegistrationControlResponse.from_control_response( response @@ -940,17 +943,20 @@ def custom_function_registration_destination( FunctionCatalogControlPayload, ) - if not self.is_connected() and not self.connect( - operation_deadline=operation_deadline + deadline = operation_deadline or OperationDeadline.after_milliseconds( + self.config.control_timeout_ms, + operation="custom registration destination", + ) + if not self.is_connected() and not self.connect_existing( + timeout=deadline.cap_seconds(1.0), ): - raise RuntimeError("Failed to connect to execution server") - payload = FunctionCatalogControlPayload.from_request(request).to_dict() - response = ( - self._send_control_request(payload) - if operation_deadline is None - else self._send_control_request( - payload, timeout_ms=operation_deadline.remaining_milliseconds() + raise RuntimeError( + "Registration destination requires an existing execution endpoint." ) + payload = FunctionCatalogControlPayload.from_request(request).to_dict() + response = self._send_control_request( + payload, + timeout_ms=deadline.remaining_milliseconds(), ) return ( CustomFunctionRegistrationDestinationControlResponse.from_control_response( diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 9996f99bdf..15f813d5af 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -407,8 +407,8 @@ def register( ) def finish_job(ref): - deadline = time.monotonic() + 45 - while time.monotonic() < deadline: + # call() enforces the single existing 240s journey/10s observation bounds. + while True: status = call( "openhcs_get_execution_status", {"job_id": ref.job_id, "timeout_ms": 1000}, @@ -420,9 +420,6 @@ def finish_job(ref): time.sleep( 0.2 ) # Existing submitted-job lifecycle observation, never a submit replay. - raise RuntimeError( - "Job observation bound exhausted; retain handles and original job." - ) try: for role, port in ports.items(): @@ -556,15 +553,10 @@ def finish_job(ref): error_code="mcp_tool_failed", ) receipt["stale_preparation_cancel_rejected"] = True - preparation_deadline = time.monotonic() + 100 if not state.outcome.ready: register(error_code="function_catalog_not_ready") while not state.outcome.ready: assert not state.outcome.terminal, state - if time.monotonic() >= preparation_deadline: - raise RuntimeError( - "Preparation observation budget exhausted; preserve exact handle." - ) time.sleep(1) state = call( "openhcs_get_function_catalog_preparation_status", @@ -663,6 +655,7 @@ def finish_job(ref): }, OrchestratorSessionRef, ) + compile_started = time.monotonic() compiled = finish_job( call( "openhcs_submit_compile", @@ -671,6 +664,8 @@ def finish_job(ref): ) ) receipt["compile_response"] = to_jsonable(compiled) + receipt["compile_elapsed_seconds"] = time.monotonic() - compile_started + execution_started = time.monotonic() executed = finish_job( call( "openhcs_submit_pipeline_execution", @@ -679,6 +674,7 @@ def finish_job(ref): ) ) receipt["execution_response"] = to_jsonable(executed) + receipt["execution_elapsed_seconds"] = time.monotonic() - execution_started output_images = list((owned / "outputs").rglob("*.tif")) assert output_images, inspected from openhcs.core.image_file_serialization import ImageFileFormat @@ -834,6 +830,7 @@ def finish_job(ref): for handle in handles: handle.close() receipt["runtime_terminal"] = True + receipt["journey_elapsed_seconds"] = time.monotonic() - started save() fcntl.flock(lock, fcntl.LOCK_UN) lock.close() diff --git a/tests/runtime_diagnostics/registration_attach_only_20260929/pytest.txt b/tests/runtime_diagnostics/registration_attach_only_20260929/pytest.txt new file mode 100644 index 0000000000..d2009d14ea --- /dev/null +++ b/tests/runtime_diagnostics/registration_attach_only_20260929/pytest.txt @@ -0,0 +1,16 @@ +openhcs=/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py +polystore=/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py +......................................................... [100%] +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +57 passed, 25 deselected, 2 warnings in 7.22s diff --git a/tests/runtime_diagnostics/registration_attach_only_20260929/resources.txt b/tests/runtime_diagnostics/registration_attach_only_20260929/resources.txt new file mode 100644 index 0000000000..a02dbdb4d5 --- /dev/null +++ b/tests/runtime_diagnostics/registration_attach_only_20260929/resources.txt @@ -0,0 +1 @@ +elapsed=8.04 max_rss_kib=309668 exit=0 diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-control.jsonl new file mode 100644 index 0000000000..d888b678e9 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-control.jsonl @@ -0,0 +1,3 @@ +{"event": "native_imports", "paths": {"openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py"}} +{"type": "openhcs_custom_function_registration_destination", "time": 1790725944.628144} +{"type": "force_shutdown", "time": 1790726194.249227} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-stderr.txt new file mode 100644 index 0000000000..13b3d72b80 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-stderr.txt @@ -0,0 +1,20 @@ +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15995 (control: 16995) +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign.log +2026-09-29 19:52:14,549 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:15,017 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15995 (PUB), control tcp://127.0.0.1:16995 +2026-09-29 19:52:15,018 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:52:15,018 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:52:15,018 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:52:15,049 - zmqruntime.server - INFO - Server ready +2026-09-29 19:56:34,266 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:56:34,266 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:56:34,277 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:56:34,277 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:56:34,277 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/journey.json b/tests/runtime_diagnostics/registration_live_20260929_attempt02/journey.json new file mode 100644 index 0000000000..f7368cc979 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/journey.json @@ -0,0 +1,7730 @@ +{ + "accepted": false, + "source_sha": "4c6fbac0a0edb7805b36f66e9c5a56d7d2cd58f5", + "imports": { + "openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", + "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py" + }, + "submodules": [ + " b0004daf3223326ac78689ba7b2e68abc18ae298 external/ObjectState (v1.1.8)", + " 1209068f5fc9e4f7025a24ada8a73ef30dd1ddd7 external/PolyStore (v0.2.19-11-g1209068)", + " fddd9857bcd21468d20bc8547a769e0c6c9f1906 external/arraybridge (v0.3.4)", + " ca0a87e873f929b311a87a4d60cd3bfba315dbcf external/metaclass-registry (v0.2.1)", + " 792b03413331fdf107e369c924c257a2f0b16126 external/pycodify (v0.1.3)", + " a51c3d965f8ffdefe95a2b4b37347a85341d483a external/pyqt-reactive (v0.3.24)", + " 4e8f8bde5f750d9a50eacac80acd3806123addd8 external/python-introspect (v0.1.14-1-g4e8f8bd)", + " 1d3b32f4fcead23d5d2079c975eb2ae7646a1478 external/zmqruntime (v0.2.24)" + ], + "resources": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.4, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 12.0, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "driver_pid": 1876976, + "source_live_not_installed": true, + "installed_entrypoint": false, + "launch_cwd": "/home/ts/wt/openhcs-custom-function-admission-20260929", + "calls": [ + { + "tool": "openhcs_health_check", + "arguments": {}, + "started_utc": 1790725942.4334848, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_health_check", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "status": "ok", + "started_at_unix": 1790725941.5992095, + "service": "openhcs.mcp", + "openhcs_version": "0.8.6", + "packaged_resources_ready": true, + "packaged_resource_count": 56, + "missing_packaged_resource_paths": [], + "server_process_id": 1877427, + "server_source_path": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/mcp/server.py", + "server_import_mtime_ns": 1790721541146819947, + "server_current_mtime_ns": 1790721541146819947, + "server_source_changed_since_import": false, + "stale_source_paths": [], + "recovery_reason": "current", + "installation_pointer_path": null, + "installation_pointer_changed_since_import": false, + "installation_pointer_available": null, + "restart_required": false, + "restart_command": [], + "restart_command_is_stable": false, + "reconnect_required": false, + "reconnect_owner": null, + "retry_after_reconnect": false, + "automatic_recovery_on_reconnect": false, + "restart_hint": null + } + ] + } + ] + }, + "elapsed_seconds": 0.009950784005923197 + }, + { + "tool": "openhcs_get_authoring_context", + "arguments": { + "kind": "first_use" + }, + "started_utc": 1790725942.5051858, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_authoring_context", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "kind": "first_use", + "content": "=== CHOOSE AN OPENHCS WORKFLOW ===\nUse this context when you are starting or resuming work and need to choose the OpenHCS state owner or next workflow.\n\n=== CORE MODEL IN ONE PASS ===\n- OpenHCS is a typed compiler/runtime for high-content microscopy. It projects microscope data into a storage-independent virtual workspace, compiles PipelineConfig plus ordered FunctionStep declarations, resolves multidimensional grouping and artifact contracts, executes through typed runtime values, and validates results through plate and viewer evidence.\n- Choose the state owner before choosing file formats or tools. If the user asks to open, show, continue, or edit work in the desktop, the UI-owned route always takes precedence over folder inspection or a headless session.\n\n=== CHOOSE ONE TASK ROUTE ===\nRequest exactly one matching context with openhcs_get_authoring_context; deepen into another only when the workflow reaches that boundary. Reuse this router after a handoff or a change of task; first_use is not a one-time setup step.\n- kind=\"ui_visible_workflow\" \u2014 Work in the running desktop UI: the user asks to open, show, continue, or edit work in the visible OpenHCS desktop.\n- kind=\"domain_expert_assisted_setup\" \u2014 Set up a workflow from biological intent: a domain expert describes the experiment and expected result but wants the agent to handle OpenHCS mechanics.\n- kind=\"folder_onboarding\" \u2014 Inspect and onboard image data: the task starts from a local plate, image folder, or rich image container and does not require visible UI ownership.\n- kind=\"pipeline\" \u2014 Author or revise a typed pipeline: the sources are understood and the next task is choosing functions, axes, grouping, configuration, or artifact contracts.\n- kind=\"image_analysis_workflow\" \u2014 Design and validate an image-analysis workflow: an analysis needs multisite assembly, registration, normalisation, mosaic quality control, segmentation review, or image-result provenance.\n- kind=\"custom_function\" \u2014 Author a custom processing function: the required operation is not already in the function registry or needs a typed custom artifact contract.\n- kind=\"headless_execution\" \u2014 Compile and execute headlessly: a reviewed pipeline should run without becoming selected or editable in the desktop UI.\n- kind=\"debugging\" \u2014 Diagnose compiled and runtime behavior: a compile, execution, step output, runtime value, or artifact must be inspected at its owning boundary.\n- kind=\"viewer_review\" \u2014 Validate results in a managed viewer: execution produced images, labels, or ROIs that need bounded visual and structured validation.\n- kind=\"objectstate_editing\" \u2014 Inspect or edit typed UI state: one exact UI-backed field, inheritance value, snapshot, or branch must be inspected or changed.\n- kind=\"cellprofiler_translation\" \u2014 Translate or validate a CellProfiler workflow: the intent begins with a .cppipe, CellProfiler module vocabulary, or an exact parity claim.\n\n=== SAFE FIRST ACTION ===\n- Start read-only. Query the active surface through openhcs_search_capabilities with task-relevant workflow, target, or text filters; its registry-owned metadata is the authority for what this server exposes. Use openhcs_list_capabilities only when the complete selected surface is required.\n- Guides describe workflows across capability profiles. Confirm a named tool is exposed before calling it. Desktop actions are discovered separately through openhcs_ui_list_actions; an empty capability search does not establish that a running window lacks the action.\n- Before operating a live desktop workflow, call openhcs_ui_list_windows and resolve visible modal top-level windows first. Inspect each dialog through openhcs_ui_get_widget_tree and invoke its declared button through openhcs_ui_invoke_widget_action; acknowledge completed operations, cancel unsolicited update/restart prompts, and never use desktop-coordinate automation or leave a blocking dialog hidden behind the analysis viewer.\n- Request the matching task context before mutation. Do not read every knowledge document, enumerate every function, invent config fields, or load full-resolution image data up front.\n- When continuing an analysis, read and reuse its existing pipeline and configuration. Adapt only the required source/layout choices, preserve earlier outputs in a separate destination, and record unavailable acquisitions as missing rather than zero measurements. Validate a representative bounded run before expanding.\n- If returned guidance is truncated, retrieve the same context with a sufficient max_chars before acting on an incomplete workflow.\n- Before any write or execution, show the exact target and intended change, refresh revision/request tokens, obtain approval, validate, and compile before running.\n\n=== DEEPEN ONLY WHEN NEEDED ===\nRetrieve one relevant source-backed target with openhcs_get_knowledge_document; use its returned section ids to narrow further instead of loading every document.\n- openhcs_architecture_quick_start \u2014 OpenHCS architecture quick start: Front-door OpenHCS architecture and MCP route covering the public PipelineConfig plus FunctionStep model, ObjectState resolution, function patterns, axes and grouping, typed artifacts, CellProfiler translation, generic source bindings, compilation, runtime values, examples, and safe operation." + } + ] + } + ] + }, + "elapsed_seconds": 1.3069605819764547 + }, + { + "tool": "openhcs_get_authoring_context", + "arguments": { + "kind": "custom_function" + }, + "started_utc": 1790725943.8703487, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_authoring_context", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "kind": "custom_function", + "content": "=== AUTHOR A CUSTOM PROCESSING FUNCTION ===\nUse this context when the required operation is not already in the function registry or needs a typed custom artifact contract.\n\n=== CUSTOM FUNCTIONS AND RUNTIME OUTPUTS ===\n- Custom functions are registry functions used inside FunctionStep.func patterns: callable, (callable, kwargs), list chains, or component/group keyed patterns.\n- A reviewed custom function becomes an ordinary registry-described callable; it does not create a separate pipeline mechanism. If openhcs_search_functions has no contract-compatible implementation for an intended operation, use this route, register the callable, then describe its reflected contract before composing it.\n- CLAHE, median denoising, background subtraction, and flat-field correction are example preprocessing intents, not a promised built-in catalog. Implement only the missing operation, keep it backend-agnostic at the pipeline boundary, and let its memory decorator, ProcessingContract, and artifact declarations state compatibility.\n- Dict patterns are routed by processing_config.group_by; list and single-callable patterns apply the same callable chain to each grouped 3D array.\n- processing_config.variable_components are the axes stacked into each callable input array; group_by groups the remaining 3D arrays and only selects callable branches for dict patterns.\n- Memory decorators such as @numpy define runtime memory conversion; FunctionStep processing_config defines grouping, axes, and input source.\n- Declare outputs with typed ArtifactSpec values. The decorator owns their input/output role. MeasurementsArtifactType receives schema-bearing ColumnarRows; ObjectLabelsArtifactType receives complete integer label arrays and may use segmentation_mask_rois for materialization.\n- Path-bearing topology belongs in one SpatialGraphArtifactType output whose SpatialGraph nodes, edges, and scalar features are authoritative. A directed morphology forest can materialize through SWCOptions while SpatialGraphROIOptions projects those same paths and features to Napari; do not reduce graph semantics to a mask or rebuild them in viewer code.\n- Artifact graph planning owns output names, sidecar results, materialization intent, and downstream artifact inputs; do not encode those as ad hoc files or hidden globals. Request the viewer-review context only after execution produces artifacts to inspect.\n\n=== CUSTOM FUNCTION RULES ===\n- Decorate exactly one callable with a memory decorator such as @numpy.\n- The first image-like argument is supplied by OpenHCS at runtime; do not pass it as a FunctionStep kwarg.\n- Prefer concrete typed parameters with serializable defaults.\n- Preserve dtype unless intentionally changing representation.\n- Declare measurements as MeasurementsArtifactType and return ColumnarRows; declare segmentation as ObjectLabelsArtifactType and return the complete integer label array.\n- Declare path topology as SpatialGraphArtifactType and return one SpatialGraph; use SWCOptions and SpatialGraphROIOptions as projections of that same value rather than independent graph and ROI computations.\n- Do not close over GUI, filesystem, or live viewer state.\n\n=== MINIMAL NUMPY CUSTOM FUNCTION ===\n```python\nfrom openhcs.core.memory import numpy\nimport numpy as np\n\n@numpy\ndef robust_clip(image, low_percentile: float = 1.0, high_percentile: float = 99.0):\n \"\"\"Clip intensities to robust percentiles while preserving dtype.\"\"\"\n array = np.asarray(image)\n if array.size == 0:\n return image\n low = np.percentile(array, low_percentile)\n high = np.percentile(array, high_percentile)\n if high <= low:\n return image\n return np.clip(array, low, high).astype(array.dtype, copy=False)\n```\n\n=== REGISTRATION WORKFLOW ===\n- Read the targeted custom-function and artifact-contract knowledge documents below before inventing decorators or payload types.\n- Register reviewed source through openhcs_register_custom_function; that capability delegates to the nominal CustomFunctionManager and owns persistence and registry refresh.\n- Search with openhcs_search_functions, describe the returned function_id with openhcs_describe_function, then request the pipeline context to add the function and validate its compiled artifact plan.\n\n=== DEEPEN ONLY WHEN NEEDED ===\nRetrieve one relevant source-backed target with openhcs_get_knowledge_document; use its returned section ids to narrow further instead of loading every document.\n- openhcs_custom_functions \u2014 OpenHCS custom functions: How to add custom processing functions, memory decorators, validation, GPU custom functions, registry integration, and GUI availability.\n- openhcs_custom_function_management \u2014 OpenHCS custom function management: CustomFunctionManager lifecycle, atomic updates, module injection, reload signals, and persisted custom-function registry behavior.\n- openhcs_artifact_contract_system \u2014 OpenHCS artifact contract system: Canonical callable/module artifact contracts, graph planning, exact satisfaction, runtime observations, and materialization.\n- openhcs_custom_function_workflow \u2014 Add a missing analysis operation as a typed custom function: Add a missing operation through typed custom-function owners: explicit endpoint/name/store admission, responsive native catalogue preparation with an incarnation-bound start/status/cancel handle, single source dispatch only after readiness, postdispatch uncertainty without replay, stable imports, compilation and bounded outputs; separate technical reuse from biological acceptance." + } + ] + } + ] + }, + "elapsed_seconds": 0.13100940300500952 + }, + { + "tool": "openhcs_search_capabilities", + "arguments": { + "query": "custom function", + "limit": 5 + }, + "started_utc": 1790725944.0636058, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_search_capabilities", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "surface_profile": "full", + "query": { + "text": null, + "kind": null, + "workflow_group": null, + "workflow_stage": null, + "target_context": null, + "visibility": null, + "role": null, + "has_side_effects": null, + "side_effect_contains": null, + "offset": 0, + "limit": 5 + }, + "matched_count": 101, + "returned_count": 5, + "next_offset": 5, + "capabilities": [ + { + "name": "openhcs://capabilities", + "kind": "resource", + "title": "OpenHCS agent capability registry", + "description": "Lists the resources, tools, side effects, and extras exposed by this server.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": null, + "output_type": "AgentCapabilityRegistry" + }, + { + "name": "openhcs_sweep_viewer_endpoints", + "kind": "tool", + "title": "Sweep viewer endpoints", + "description": "Sweeps the local OpenHCS IPC directory for live viewer endpoints, probes each through the viewer-window state authority, and classifies every live viewer as owned or foreign relative to this process. Dead sockets are filtered by a bind probe; non-viewer endpoint pairs (execution, UI bridge) are excluded.", + "workflow_group": "discovery", + "workflow_stage": "diagnostic", + "target_context": "server", + "visibility": "beginner", + "role": "diagnostic", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [ + "viewer_endpoint_inventory" + ], + "security_requirements": [], + "input_type": null, + "output_type": "ViewerEndpointDiscoveryResult" + }, + { + "name": "openhcs_health_check", + "kind": "tool", + "title": "Health check", + "description": "Reports OpenHCS MCP health, installed OpenHCS version, packaged-resource readiness, server process identity, source freshness, installation-generation freshness, and the client-owned reconnect contract for stale-process diagnostics.", + "workflow_group": "discovery", + "workflow_stage": "diagnostic", + "target_context": "server", + "visibility": "beginner", + "role": "diagnostic", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [ + "installed_openhcs_version", + "packaged_resource_readiness", + "packaged_resource_paths", + "mcp_process_identity", + "mcp_source_freshness", + "mcp_installation_generation" + ], + "security_requirements": [], + "input_type": null, + "output_type": "McpServerHealthResult" + }, + { + "name": "openhcs_list_capabilities", + "kind": "tool", + "title": "List capabilities", + "description": "Returns the canonical agent capability registry.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": null, + "output_type": "AgentCapabilityRegistry" + }, + { + "name": "openhcs_search_capabilities", + "kind": "tool", + "title": "Search capabilities", + "description": "Returns a bounded task-specific projection of the selected canonical capability registry. Filter by registry-owned workflow group, stage, target context, visibility, role, side effects, or free text before choosing tools.", + "workflow_group": "discovery", + "workflow_stage": "discovery", + "target_context": "server", + "visibility": "beginner", + "role": "primary", + "read_only": true, + "side_effects": [], + "requires_network": false, + "required_extras": [], + "runtime_requirements": [], + "data_exposure": [], + "security_requirements": [], + "input_type": "AgentCapabilitySearchRequest", + "output_type": "AgentCapabilitySearchResult" + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.009299249009927735 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n" + }, + "started_utc": 1790725944.1412935, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Custom function registration requires an explicit port.", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "ValueError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.009471785015193745 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign", + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.218453, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.007558557990705594 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/ancestor-escape", + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.2866933, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.007576522999443114 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions", + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.3598785, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "agent_path_policy_rejected", + "message": "Writable path is outside allowed roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign/registration_live_probe.py. Effective writable roots: /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned.", + "hint": "Pass a local path under OPENHCS_AGENT_READ_ROOTS or OPENHCS_AGENT_WRITE_ROOTS as appropriate. Use openhcs_inspect_plate_path first to validate a plate folder.", + "exception_type": "AgentPathPolicyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.013567510002758354 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions", + "port": 15995, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.4369924, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Selected endpoint owns custom storage /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign/data/openhcs/custom_functions, source /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign/data/openhcs/custom_functions/registration_live_probe.py, not requested /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions/registration_live_probe.py; no source was dispatched.", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "ValueError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.1938281979819294 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions", + "port": 15996, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.695305, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "{'status': 'error', 'type': 'error', 'message': 'Unsupported destination proof fixture'}", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "RuntimeError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.15166049200342968 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions", + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725944.9131825, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "function_catalog_not_ready", + "message": "Catalog preparation FunctionCatalogPreparationHandle(connection=ExecutionConnectionSpec(host='127.0.0.1', port=15994, transport_mode=, persistent=True), server_identity=ProcessIdentity(pid=1877140, create_time=1790725928.37)) is not_started: Function catalog has not been requested. No source was dispatched.", + "hint": "Start/observe the same typed catalog preparation handle; no source was dispatched.", + "exception_type": "FunctionCatalogNotReadyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.15642607500194572 + }, + { + "tool": "openhcs_start_function_catalog_preparation", + "arguments": { + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp" + }, + "started_utc": 1790725945.1442478, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_start_function_catalog_preparation", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019741033000173047 + }, + { + "tool": "openhcs_cancel_function_catalog_preparation", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 0 + } + }, + "started_utc": 1790725945.2257223, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_cancel_function_catalog_preparation", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_cancel_function_catalog_preparation", + "errors": [ + { + "code": "mcp_tool_failed", + "message": "Function catalog preparation owner changed; handle is stale.", + "hint": "The MCP server caught this exception at the tool boundary.", + "exception_type": "RuntimeError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.019519852998200804 + }, + { + "tool": "openhcs_register_custom_function", + "arguments": { + "source_code": "\"\"\"Synthetic identity-plus-three contract; no file IO or biological semantics.\"\"\"\n\nfrom openhcs.core.memory import numpy\n\n\n@numpy\ndef registration_live_probe(image, offset=3):\n return image + offset\n", + "function_name": "registration_live_probe", + "storage_dir": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data/openhcs/custom_functions", + "port": 15994, + "host": "127.0.0.1", + "transport_mode": "tcp", + "persist": true + }, + "started_utc": 1790725945.31083, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_register_custom_function", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "ok": false, + "tool": "openhcs_register_custom_function", + "errors": [ + { + "code": "function_catalog_not_ready", + "message": "Catalog preparation FunctionCatalogPreparationHandle(connection=ExecutionConnectionSpec(host='127.0.0.1', port=15994, transport_mode=, persistent=True), server_identity=ProcessIdentity(pid=1877140, create_time=1790725928.37)) is pending: Preparing function catalog and declared kernel caches. No source was dispatched.", + "hint": "Start/observe the same typed catalog preparation handle; no source was dispatched.", + "exception_type": "FunctionCatalogNotReadyError", + "path": null + } + ] + } + ] + } + ] + }, + "elapsed_seconds": 0.036365978012327105 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725946.4218647, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013632312999106944 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725947.5078082, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.014705075998790562 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725948.5923054, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.018149595998693258 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725949.6819053, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02612920297542587 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725950.782732, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.011742689996026456 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725951.8760407, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016320899012498558 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725952.9753315, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013631792011437938 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725954.0740933, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013278522004839033 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725955.1610482, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01229845397756435 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725956.2481637, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02160134000587277 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725957.3515923, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.014992260985309258 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725958.4485323, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.025714496994623914 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725959.5482492, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02350493799895048 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725960.6398494, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01994419700349681 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725961.731657, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.012471902009565383 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725962.8219976, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.014474980009254068 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725963.9309413, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.014432830008445308 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725965.527001, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01461368301534094 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725966.6342132, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01812486900598742 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725967.759359, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016731146984966472 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725968.8573766, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.017604171996936202 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725969.9596906, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013895932992454618 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725971.0544791, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019906897010514513 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725972.1539621, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.020844534010393545 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725973.2829902, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02364578499691561 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725974.4001815, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016399077023379505 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725975.4978924, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.021906988986302167 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725976.617755, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01982949100784026 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725977.7520185, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.018733482982497662 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725978.9030495, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016134165023686364 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725980.251429, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.012884805008070543 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725981.3585713, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01914357001078315 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725982.4726999, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.020488088979618624 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725983.5919654, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016515819006599486 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725984.7082696, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02012346699484624 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725985.8380418, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.022962618997553363 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725986.9601324, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.018416490987874568 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725988.0930614, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.020093189988983795 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725989.2104504, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.023230597988003865 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725990.3249526, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.022541210986673832 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725991.459577, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02419164901948534 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725992.5870695, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.014520466007525101 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725993.6957674, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.018396223982563242 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725994.805088, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.009939481999026611 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725995.9076688, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01843480599927716 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725997.0347424, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.012077925988705829 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725998.1595125, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.023367167013930157 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790725999.3054795, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019783081981586292 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726000.4384606, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019152335997205228 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726001.5523055, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02190543498727493 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726002.6846435, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.011347020015818998 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726003.8103917, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.022081428993260488 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726004.954244, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019776539993472397 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726006.0938456, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013376126007642597 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726007.2122376, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.022256030002608895 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726008.350096, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02125334899756126 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726009.4817917, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019601006992161274 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726010.6134744, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01778990399907343 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726011.7605777, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01484952899045311 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726012.8984656, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.010255770990625024 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726014.017237, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02159863300039433 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726015.1550694, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01752980099990964 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726016.28423, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01888160197995603 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726017.4166434, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019336535973707214 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726018.5525494, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.017142947006504983 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726019.682027, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016369971999665722 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726020.8137176, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019583793997298926 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726021.9527614, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.016552668006625026 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726023.0951836, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01608225700329058 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726024.2384403, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.024532393988920376 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726025.395595, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.023884405993157998 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726026.5397196, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01092800498008728 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726027.6632855, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.022553502989467233 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726028.8179693, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.017575806996319443 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726029.9653265, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02034565800568089 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726031.1198797, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02407104999292642 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726032.268305, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.024656059016706422 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726033.419815, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.023066456022206694 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726034.5670688, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01293841699953191 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726035.682749, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01602269400609657 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726036.8269506, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01895371900172904 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726037.9690642, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.020973087986931205 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726039.1216767, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.018121911998605356 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726040.2697654, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.013876956014428288 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726041.4053273, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.017223991017090157 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726042.539506, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.01285576997906901 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726043.6755073, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.019571811018977314 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726044.844064, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.02216780200251378 + }, + { + "tool": "openhcs_get_function_catalog_preparation_status", + "arguments": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "started_utc": 1790726045.9986656, + "status": "returned", + "response": { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "pending", + "progress": { + "phase": "preparing_capabilities", + "message": "Preparing function catalog and declared kernel caches", + "sequence": 2, + "timestamp": 1790725945.1608434 + } + } + ] + } + ] + }, + "elapsed_seconds": 0.015043006016639993 + } + ], + "processes": [ + { + "role": "owned", + "port": 15994, + "identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + { + "role": "foreign", + "port": 15995, + "identity": { + "pid": 1877161, + "create_time": 1790725931.41 + } + }, + { + "role": "unsupported", + "port": 15996, + "identity": { + "pid": 1877225, + "create_time": 1790725934.23 + } + } + ], + "original033": "preserved; not replayed", + "shared7777": "never contacted", + "uncertain_dispatch": null, + "scratch": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02", + "resource_observations": [ + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.4, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.4, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3188 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3328 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 3215 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 5326 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 5326 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 5326 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 5326 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 5326 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 934290 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 934290 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 934290 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 934290 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 934290 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 11.0 GiB" + ], + "swap_used_gib": 11.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 11.3 GiB" + ], + "swap_used_gib": 11.3 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 11.5 GiB" + ], + "swap_used_gib": 11.5 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 11.6 GiB" + ], + "swap_used_gib": 11.6 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 11.7 GiB" + ], + "swap_used_gib": 11.7 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.8 GiB" + ], + "swap_used_gib": 11.8 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 11.8 GiB" + ], + "swap_used_gib": 11.8 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 11.9 GiB" + ], + "swap_used_gib": 11.9 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 12.0 GiB" + ], + "swap_used_gib": 12.0 + }, + "scratch_bytes": 1037446 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 1075999 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 1298672 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 1677327 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 2079697 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 2496580 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 2683546 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 2971868 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 3318251 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 3617614 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 4274736 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 4670711 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 4834239 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 5055135 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 5530219 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 5866520 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6077365 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6077365 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6499279 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6499279 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.0, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6712778 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.0, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6712778 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.9, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 6948809 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.9, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 7076655 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 7249756 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.8, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 7429205 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 7602501 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 8025843 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 8458502 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 8489564 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 8966157 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 9496371 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 9913580 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 10073413 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 10251090 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 10251090 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 10542978 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 10891663 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 11070358 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 11147204 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 11467676 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 11701656 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 10.7, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 11779886 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.0, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 12306598 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 12474107 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.1, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 12562450 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 12759791 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.2, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 12938953 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 13072739 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.3, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 13218103 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 13543343 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.4, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 13737363 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 13958014 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 14153803 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.5, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 14329180 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 14490436 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 14623616 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 14938986 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 15173905 + }, + { + "guard": { + "free_gib": { + "/": 8.6, + "/dev/shm": 15.4, + "/home": 20.3, + "/tmp": 9.6 + }, + "level": "warning", + "ram_available_gib": 11.6, + "reasons": [ + "swap used 12.2 GiB" + ], + "swap_used_gib": 12.2 + }, + "scratch_bytes": 15405256 + } + ], + "mcp_pid": 1877427, + "mcp_launch_environment": { + "HOME": "/home/ts", + "LOGNAME": "ts", + "PATH": "/home/ts/.codex/packages/standalone/releases/0.159.1-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0ZH6jHa:/home/ts/.codex/tmp/arg0/codex-arg004m4pr:/home/ts/.codex/packages/standalone/releases/0.159.0-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0ChXg03:/home/ts/.codex/packages/standalone/releases/0.158.0-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0a3jKy1:/home/ts/.codex/packages/standalone/releases/0.157.1-x86_64-unknown-linux-musl/codex-path:/home/ts/.codex/tmp/arg0/codex-arg0fLf3ul:/usr/lib/openai-codex/codex-path:/home/ts/.pyenv/bin:/home/ts/bin:/usr/local/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/home/ts/.pyenv/bin:/home/ts/bin:/usr/local/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/home/ts/.elan/bin:/home/ts/programs/Brayns/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/opt/android-sdk/emulator:/opt/android-ndk:/opt/android-sdk/build-tools/30.0.3:/opt/cuda/bin:/home/ts/.local/share/flatpak/exports/bin:/var/lib/flatpak/exports/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/.dotfiles/revsl.sh:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/code/scripts/bar:/home/ts/.local/bin:/root/.local/bin:/home/ts/bin:/usr/local/bin:/home/ts/.dotfiles/revsl.sh:/opt/android-sdk/emulator:/opt/android-ndk", + "SHELL": "/bin/zsh", + "TERM": "dumb", + "USER": "ts", + "QT_QPA_PLATFORM": "offscreen", + "DISPLAY": ":0", + "XAUTHORITY": "/home/ts/.Xauthority", + "XDG_RUNTIME_DIR": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/runtime", + "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus", + "XDG_SESSION_TYPE": "x11", + "XDG_DATA_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/data", + "XDG_CACHE_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/cache", + "XDG_CONFIG_HOME": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/config", + "PYTHONPATH": "/home/ts/wt/openhcs-custom-function-admission-20260929:/home/ts/wt/openhcs-custom-function-admission-20260929/external/ObjectState/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/arraybridge/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/metaclass-registry/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pycodify/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pyqt-reactive/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/python-introspect/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/zmqruntime/src", + "OPENHCS_AGENT_READ_ROOTS": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02:/home/ts/wt/openhcs-custom-function-admission-20260929/packaging", + "OPENHCS_AGENT_WRITE_ROOTS": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned", + "OPENHCS_UI_CONFIG_CACHE_FILE": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/ui_config.config", + "OPENHCS_CPU_ONLY": "true", + "OPENHCS_HEADLESS": "true", + "NUMBA_CACHE_DIR": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/owned/numba", + "OPENHCS_USE_THREADING": "true", + "POLYSTORE_IMAGEJ_CACHE_ROOT": "/home/ts/.cache/polystore/imagej", + "POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD": "false", + "OMP_NUM_THREADS": "1", + "OPENBLAS_NUM_THREADS": "1", + "MKL_NUM_THREADS": "1", + "NUMEXPR_NUM_THREADS": "1", + "BLIS_NUM_THREADS": "1", + "NUMBA_NUM_THREADS": "1" + }, + "protected_sentinel": { + "path": "/home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt02/foreign/registration_live_probe.py", + "sha256_before_after": "748cc865eb250527979464908234f357d1ddf6eceecc50057eed58df7c911b79", + "unchanged": true + }, + "preparation_handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "stale_preparation_cancel_rejected": true, + "failure": { + "type": "RuntimeError", + "message": "Preparation observation budget exhausted; preserve exact handle.", + "traceback": "Traceback (most recent call last):\n File \"/home/ts/wt/openhcs-custom-function-admission-20260929/tests/diagnostics/check_custom_registration_live.py\", line 565, in run\n raise RuntimeError(\nRuntimeError: Preparation observation budget exhausted; preserve exact handle.\n" + }, + "same_handle_observations": [ + { + "server": { + "command": "/home/ts/code/projects/openhcs/.venv/bin/python", + "module": "openhcs.mcp" + }, + "errors": [], + "results": [ + { + "tool": "openhcs_get_function_catalog_preparation_status", + "mcp_error": false, + "payloads": [ + { + "schema_version": "openhcs.agent.v1", + "errors": [], + "warnings": [], + "handle": { + "connection": { + "host": "127.0.0.1", + "port": 15994, + "transport_mode": "tcp", + "persistent": true + }, + "server_identity": { + "pid": 1877140, + "create_time": 1790725928.37 + } + }, + "outcome": "ready", + "progress": { + "phase": "connected", + "message": "Function catalog ready", + "sequence": 6, + "timestamp": 1790726055.660598 + } + } + ] + } + ] + } + ], + "operator_disposition": "Close only diagnostic owned processes; do not replay any recorded call.", + "client_closed": true, + "closed_servers": [ + { + "role": "unsupported", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + }, + { + "role": "foreign", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + }, + { + "role": "owned", + "shutdown": { + "succeeded": true, + "endpoint_terminated": true + }, + "returncode": 0 + } + ], + "runtime_terminal": true +} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/mcp-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt02/mcp-stderr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-control.jsonl new file mode 100644 index 0000000000..2c1d4b21a2 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-control.jsonl @@ -0,0 +1,98 @@ +{"event": "native_imports", "paths": {"openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py"}} +{"type": "openhcs_custom_function_registration_destination", "time": 1790725945.0565147} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725945.0676496} +{"type": "openhcs_function_catalog_prepare", "time": 1790725945.1603625} +{"type": "openhcs_function_catalog_preparation_cancel", "time": 1790725945.2431324} +{"type": "openhcs_custom_function_registration_destination", "time": 1790725945.3353221} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725945.3461604} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725946.4348035} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725947.521267} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725948.6090548} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725949.706485} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725950.7945244} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725951.8919444} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725952.9892557} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725954.0865998} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725955.1729884} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725956.270118} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725957.3673756} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725958.4747596} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725959.5713673} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725960.6594055} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725961.7466016} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725962.8357959} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725963.9459987} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725965.5427525} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725966.653514} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725967.775487} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725968.875638} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725969.9751809} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725971.0750012} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725972.1750734} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725973.3147376} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725974.4169014} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725975.5193086} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725976.6422105} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725977.7761443} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725978.922314} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725980.2665021} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725981.379145} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725982.5003104} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725983.6117218} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725984.7330527} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725985.864213} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725986.984451} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725988.1166081} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725989.2377057} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725990.348807} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725991.4920843} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725992.6048312} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725993.7170646} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725994.8177562} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725995.929161} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725997.0495994} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725998.190302} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790725999.32981} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726000.4608407} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726001.5815823} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726002.7002165} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726003.839725} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726004.9803023} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726006.1111438} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726007.2426813} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726008.3763037} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726009.5073333} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726010.638207} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726011.7823439} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726012.9133728} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726014.0468552} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726015.177818} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726016.309998} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726017.4405713} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726018.5812442} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726019.712044} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726020.8423796} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726021.9820192} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726023.1226983} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726024.2738585} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726025.424494} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726026.5567365} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726027.6972005} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726028.8468223} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726029.996502} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726031.157095} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726032.2971299} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726033.4575956} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726034.5869844} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726035.7059975} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726036.856017} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726037.994711} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726039.1541295} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726040.2925549} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726041.4298224} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726042.559062} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726043.7085617} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726044.8794377} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726046.0203078} +{"type": "openhcs_function_catalog_preparation_status", "time": 1790726096.6193945} +{"type": "force_shutdown", "time": 1790726194.7410457} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-stderr.txt new file mode 100644 index 0000000000..00acd3ec1a --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-stderr.txt @@ -0,0 +1,29 @@ +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15994 (control: 16994) +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt02/owned.log +2026-09-29 19:52:11,560 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:12,049 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15994 (PUB), control tcp://127.0.0.1:16994 +2026-09-29 19:52:12,049 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:52:12,049 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:52:12,049 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:52:12,212 - zmqruntime.server - INFO - Server ready +2026-09-29 19:54:11,526 - openhcs.processing.backends.lib_registry.registry_service - INFO - Registry metadata cache cleared +2026-09-29 19:54:11,526 - openhcs.processing.custom_functions.manager - INFO - Loaded 0 persisted custom function(s) +2026-09-29 19:54:11,543 - openhcs.processing.backends.lib_registry.registry_service - INFO - CPU-only registry discovery skipping CupyRegistry +2026-09-29 19:54:11,543 - openhcs.processing.backends.lib_registry.registry_service - INFO - CPU-only registry discovery skipping PyclesperantoRegistry +2026-09-29 19:54:15,185 - openhcs.processing.backends.lib_registry.unified_registry - INFO - 🔄 load_or_discover_functions called for openhcs +2026-09-29 19:54:15,189 - openhcs.processing.backends.lib_registry.unified_registry - INFO - ✅ Loaded 152 openhcs functions from cache +2026-09-29 19:54:15,189 - openhcs.processing.backends.lib_registry.unified_registry - INFO - 🔄 load_or_discover_functions called for skimage +2026-09-29 19:54:15,189 - openhcs.processing.backends.lib_registry.unified_registry - INFO - ✅ Loaded 114 skimage functions from cache +2026-09-29 19:54:15,189 - openhcs.processing.backends.lib_registry.registry_service - INFO - Total functions discovered: 266 +2026-09-29 19:56:34,755 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:56:34,755 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:56:34,766 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:56:34,766 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:56:34,766 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-control.jsonl b/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-control.jsonl new file mode 100644 index 0000000000..90f9f694e3 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-control.jsonl @@ -0,0 +1,3 @@ +{"event": "native_imports", "paths": {"openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py"}} +{"type": "openhcs_custom_function_registration_destination", "time": 1790725944.8458376} +{"type": "force_shutdown", "time": 1790726193.765926} diff --git a/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-stderr.txt b/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-stderr.txt new file mode 100644 index 0000000000..b2dd7ff2f9 --- /dev/null +++ b/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-stderr.txt @@ -0,0 +1,20 @@ +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - ZMQ Execution Server +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log level: INFO (from --log-level=INFO) +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Port: 15996 (control: 16996) +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Host: 127.0.0.1 +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Transport mode: tcp +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Persistent: True +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - Log file: /home/ts/wt/openhcs-custom-function-admission-20260929/tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported.log +2026-09-29 19:52:17,415 - openhcs.runtime.zmq_execution_server_launcher - INFO - ============================================================ +2026-09-29 19:52:17,897 - zmqruntime.server - INFO - ZMQ Server started on tcp://127.0.0.1:15996 (PUB), control tcp://127.0.0.1:16996 +2026-09-29 19:52:17,897 - zmqruntime.execution.server - INFO - Queue worker thread started - will process executions sequentially +2026-09-29 19:52:17,897 - zmqruntime.execution.server - INFO - Started execution queue worker thread +2026-09-29 19:52:17,897 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server ready - waiting for requests... +2026-09-29 19:52:18,070 - zmqruntime.server - INFO - Server ready +2026-09-29 19:56:33,776 - zmqruntime.execution.server - INFO - No live worker processes found to kill +2026-09-29 19:56:33,776 - zmqruntime.execution.server - INFO - Workers killed (0), server shutting down +2026-09-29 19:56:33,787 - zmqruntime.server - INFO - ZMQ Server stopped +2026-09-29 19:56:33,787 - polystore.backend_registry - INFO - Backend connections cleaned up (including process resources) +2026-09-29 19:56:33,787 - openhcs.runtime.zmq_execution_server_launcher - INFO - Server stopped diff --git a/tests/unit/test_function_catalog_zmq.py b/tests/unit/test_function_catalog_zmq.py index 4c83ff5937..65409e13c8 100644 --- a/tests/unit/test_function_catalog_zmq.py +++ b/tests/unit/test_function_catalog_zmq.py @@ -6,10 +6,11 @@ import threading import time from concurrent.futures import CancelledError, Future +from contextlib import contextmanager from dataclasses import replace import pytest -from zmqruntime import OperationCancellation +from zmqruntime import OperationCancellation, OperationDeadline from zmqruntime.client import EndpointConnectionPolicy from zmqruntime.execution import ExecutionServer from zmqruntime.messages import ProcessIdentity @@ -455,7 +456,8 @@ def test_registration_client_sends_mutation_once_even_if_pending_or_uncertain( observed = [] result = CustomFunctionRegistrationResult(schema_version="openhcs.agent.v1") - def send(payload): + def send(payload, *, timeout_ms): + assert 0 < timeout_ms <= 5000 observed.append(payload) if response_kind == "timeout": raise TimeoutError("postdispatch observation") @@ -491,6 +493,116 @@ def send(payload): assert observed == [FunctionCatalogControlPayload.from_request(request).to_dict()] +@pytest.mark.parametrize("operation", ("destination", "register")) +@pytest.mark.parametrize("endpoint_state", ("absent", "unresponsive")) +@pytest.mark.parametrize("explicit_deadline", (False, True)) +def test_registration_attach_only_preserves_missing_or_unresponsive_endpoint( + monkeypatch, + operation, + endpoint_state, + explicit_deadline, +): + """Exercise native attach-only, not a parallel fake connection lifecycle.""" + from zmqruntime.config import TransportMode + + client = ZMQExecutionClient(port=22319, transport_mode=TransportMode.TCP) + effects, attach_timeouts = [], [] + + def forbidden(effect): + def fail(*_args, **_kwargs): + effects.append(effect) + pytest.fail(f"Registration must not {effect}") + + return fail + + @contextmanager + def unlocked(*_args, **_kwargs): + yield True + + def unresponsive(timeout): + attach_timeouts.append(timeout) + return False + + monkeypatch.setattr("zmqruntime.client.endpoint_startup_lock", unlocked) + monkeypatch.setattr( + client, "_is_port_in_use", lambda port: endpoint_state == "unresponsive" + ) + monkeypatch.setattr(client, "_attach_existing_endpoint", unresponsive) + monkeypatch.setattr(client, "connect", forbidden("enter implicit startup")) + monkeypatch.setattr(client, "_spawn_server_process", forbidden("start a process")) + monkeypatch.setattr(client, "_kill_processes_on_port", forbidden("kill a process")) + monkeypatch.setattr( + client, "_send_control_request", forbidden("send a control/source request") + ) + deadline = ( + OperationDeadline.after_milliseconds(500, operation="controlled attach-only") + if explicit_deadline + else None + ) + calls = { + "destination": lambda: client.custom_function_registration_destination( + CustomFunctionRegistrationDestinationRequest( + function_name="boundary_probe" + ), + operation_deadline=deadline, + ), + "register": lambda: client.register_custom_function( + CustomFunctionRegistrationRequest( + source_code="never evaluated", persist=False + ), + operation_deadline=deadline, + ), + } + with pytest.raises(RuntimeError, match="existing execution endpoint"): + calls[operation]() + assert effects == [] + if endpoint_state == "absent": + assert attach_timeouts == [] + else: + assert len(attach_timeouts) == 1 + assert 0 < attach_timeouts[0] <= (0.5 if explicit_deadline else 1.0) + assert not client.is_connected() + + +@pytest.mark.parametrize("operation", ("destination", "register")) +def test_registration_expired_deadline_prevents_attachment_and_send( + monkeypatch, operation +): + client = ZMQExecutionClient(port=22319) + monkeypatch.setattr( + client, "connect", lambda **kwargs: pytest.fail("No implicit startup") + ) + monkeypatch.setattr( + client, + "connect_existing", + lambda **kwargs: pytest.fail("Expired before attachment"), + ) + monkeypatch.setattr( + client, + "_send_control_request", + lambda **kwargs: pytest.fail("Expired before dispatch"), + ) + deadline = OperationDeadline( + operation="already expired", timeout_ms=1, expires_at=0 + ) + calls = { + "destination": lambda: client.custom_function_registration_destination( + CustomFunctionRegistrationDestinationRequest( + function_name="boundary_probe" + ), + operation_deadline=deadline, + ), + "register": lambda: client.register_custom_function( + CustomFunctionRegistrationRequest( + source_code="never evaluated", persist=False + ), + operation_deadline=deadline, + ), + } + with pytest.raises(TimeoutError, match="already expired"): + calls[operation]() + + def test_catalog_client_applies_request_cancellation_to_endpoint_startup( monkeypatch, ) -> None: From 82f265d3ad850d90878dfa8ef997fb1d0f7b248b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 20:20:04 -0400 Subject: [PATCH 12/13] Expose responsive catalogue preparation before first-use search --- .../custom_registration_admission_20260929.md | 37 +++++++-- ...gistration_source_live_handoff_20260929.md | 75 +++++++++++++++++ openhcs/agent/services/llm_context_service.py | 20 +++++ .../codex/openhcs/skills/use-openhcs/SKILL.md | 1 + .../check_custom_registration_live.py | 35 +++++++- .../build.txt | 9 ++ .../imports.txt | 19 +++++ .../resources.txt | 1 + .../pytest.txt | 16 ++++ .../resources.txt | 1 + .../pytest.txt | 14 ++++ .../pytest_initial.txt | 83 +++++++++++++++++++ .../resources.txt | 23 +++++ .../resources_initial.txt | 24 ++++++ .../registration_onboarding_20260929/scope.md | 42 ++++++++++ .../test_progressive_authoring_context.py | 78 ++++++++++++++++- 16 files changed, 470 insertions(+), 8 deletions(-) create mode 100644 docs/plans/custom_registration_source_live_handoff_20260929.md create mode 100644 tests/runtime_diagnostics/registration_main240_build_20260929/build.txt create mode 100644 tests/runtime_diagnostics/registration_main240_build_20260929/imports.txt create mode 100644 tests/runtime_diagnostics/registration_main240_build_20260929/resources.txt create mode 100644 tests/runtime_diagnostics/registration_main240_tests_20260929/pytest.txt create mode 100644 tests/runtime_diagnostics/registration_main240_tests_20260929/resources.txt create mode 100644 tests/runtime_diagnostics/registration_onboarding_20260929/pytest.txt create mode 100644 tests/runtime_diagnostics/registration_onboarding_20260929/pytest_initial.txt create mode 100644 tests/runtime_diagnostics/registration_onboarding_20260929/resources.txt create mode 100644 tests/runtime_diagnostics/registration_onboarding_20260929/resources_initial.txt create mode 100644 tests/runtime_diagnostics/registration_onboarding_20260929/scope.md diff --git a/docs/plans/custom_registration_admission_20260929.md b/docs/plans/custom_registration_admission_20260929.md index 85ae0f68d3..3dd10b7d1f 100644 --- a/docs/plans/custom_registration_admission_20260929.md +++ b/docs/plans/custom_registration_admission_20260929.md @@ -3,8 +3,9 @@ Implementation owner: Lovelace. Integration/install owner: coordinator. Draft PR233: https://github.com/OpenHCSDev/openhcs/pull/233 Source: /home/ts/wt/openhcs-custom-function-admission-20260929. -This checkpoint follows normal main238 integration f5dbebb27 (main01d1a8c55c); -recorded PolyStore1209068 is unchanged. No installed tree is edited. +This checkpoint normally integrates main240/3032958ac at d33128916, following +main238 integration f5dbebb27 (main01d1a8c55c). Recorded PolyStore1209068 is +unchanged. No installed tree is edited. Latest source follow-up after4c6fbac0a: destination proof and source dispatch now use existing connect_existing, never attach-or-start/replacement. Each @@ -61,8 +62,30 @@ controlled-launch Manager destination lookup, start/status/cancel handle fields, READY-only mutation and precise postdispatch uncertainty. The knowledge manifest summary/tags track this; document digests remain derived, not duplicated. +Bundled SKILL.md and first_use/pipeline now point to that same existing responsive +preparation procedure before first cold function search. One nominal context +section shares the existing first-use/pipeline facets; capability names derive +from declarations. No duplicated guide, inventory or biological hints. Actual +default-context/generated MCP read-only calls reach the full canonical guide and +typed preparation capability without any endpoint-client creation. Ten source +checks pass8.20s, peak308356KiB; skill validator passes. Two existing whole-context +bound checks remain failing on unchanged image_analysis_workflow (17387chars), +also17387 with this section excluded; they are explicitly excluded from the +focused passing shard. This is not all-context readiness or installed proof. +Receipt: tests/runtime_diagnostics/registration_onboarding_20260929/. + ## Actual evidence (source, not installed acceptance) +After normal main240 integration,16 focused attach-only/deadline, real context +composition and single-send cases pass5.53s (process6.41s,peak293380KiB,exit0). +Both existing abi3 extensions built through setup.py build_ext --inplace +--parallel1 with owned temporary objects/lib:2.55s,peak146140KiB,exit0. +Imports prove source _granularity_reconstruct (45440bytes) and _tabular_native +(210808bytes), plus reviewed OpenHCS/PolyStore paths. No installation/dependency +download/JVM. Durable receipts: +tests/runtime_diagnostics/registration_main240_tests_20260929/ and +tests/runtime_diagnostics/registration_main240_build_20260929/. + Current focused provider-free shard: **47 passed, 25 deselected, 7.32 seconds**; process elapsed8.13s, peak305992KiB, exit0. Existing shared Python, explicit reviewed worktree plus eight submodule src paths, pinned shared bundle/downloadfalse, @@ -144,6 +167,9 @@ or global proof. Actual catalog witnesses: - MEMB-2/MEMB-3: control/capability membership derives from existing declarations; each new request leaf owns its operation/strategy. A new case extends that family, not an action switch or hand-maintained catalogue. + The shared onboarding section similarly uses existing nominal context facets; + first-use/pipeline reachability tests exercise registry-derived rendering and + typed capability discovery, not duplicated string-kind membership. - IDEN-8: one handle and destination token reuse ProcessIdentity; changing creation time while retaining PID rejects before cancellation/evaluation. - BOUND-1/BOUND-2/BOUND-6: existing typed request/response codecs decode at control/ @@ -174,10 +200,11 @@ McpDevServerSpec. It is implemented but not live-proved. Source mode is explicit labelled source_live_not_installed. Parent owns final merge/install and installed acceptance; no frozen installation changes are authorised here. -Parent cleared disk headroom; latest actual guard /home20.2GiB,RAM13.1GiB warns -only about historical swap. Attempt02 and the57-case source follow-up are +Latest actual guard /home19.8GiB,RAM13.1GiB warns about disk plus historical swap. +No new native journey starts while that non-swap warning remains. Attempt02, +the57-case source follow-up, main240 build/shard and onboarding source checks are terminal and released the lock. Next is the authorised finite cold source-live -journey on a fresh guard/nonblocking lock, then parent review/merge and +journey after a fresh passing non-swap guard/nonblocking lock, then parent review/merge and installation of this same frozen tree (no extra checkout). No optional hosted CI wait or installed readiness claim. diff --git a/docs/plans/custom_registration_source_live_handoff_20260929.md b/docs/plans/custom_registration_source_live_handoff_20260929.md new file mode 100644 index 0000000000..cf6d8e06d1 --- /dev/null +++ b/docs/plans/custom_registration_source_live_handoff_20260929.md @@ -0,0 +1,75 @@ +# PR233 finite source-live handoff + +Owner transfer: parent runs the next finite source-live journey, reviews/merges, +installs this same frozen tree and then proves installed entrypoint. Lovelace +has not started attempt03. No handles currently live. Do not edit this source +during the journey or after parent adopts it for installation. + +Tree: /home/ts/wt/openhcs-custom-function-admission-20260929 +Draft: https://github.com/OpenHCSDev/openhcs/pull/233 +Frozen commit: the onboarding follow-up SHA reported in the handoff message. +Normal main240 merge d33128916 / main3032958ac is included. All eight recorded +submodules remain unchanged, PolyStore1209068. Both compiled abi3 artifacts are +present in this source tree; no installation occurred. + +Source receipts: + +- registration_onboarding_20260929:10passed,2deselected,8.20s,308356KiB. + Two real-default-context/generated MCP cases reach first_use/pipeline -> + complete canonical guide -> preparation capability discovery without endpoint + contact. Skill validator passes. Initial failures and unchanged image-analysis + context overflow are preserved in scope.md / pytest_initial.txt. +- registration_main240_tests_20260929:16passed,5.53s,293380KiB. +- registration_main240_build_20260929:2.55s,146140KiB,exit0; both source extension + import paths/hashes captured. Their temporary objects/library copies and bounded + test scratch (~936KiB combined) were removed; source artifacts and receipts remain. +- registration_attach_only_20260929:57passed,25deselected,7.22s,309668KiB. + +All receipt directories are under tests/runtime_diagnostics. Original attempts01/02 +remain immutable, with original synthetic scratch retained for same-handle diagnosis. +Attempt02 status was responsive but redundant100s sub-budget ended before actual +READY at110.50s. All its endpoints/MCP/child are terminal; no mutation happened. +No warmup performance improvement or installed acceptance is claimed. + +## Next invocation + +Recheck /home >=20GiB, availableRAM >=8GiB before launch; only historical swap +warning is waived. Latest own guard was /home19.8GiB, so no native run began. +The driver itself rechecks headroom and acquires validation.lock nonblocking; +do not wrap another competing runtime/lock around it. Use a TTY and preserve +the same handle if it pauses after any unexpected disposition. It never replays. +Replace FROZEN_HEAD with the exact reviewed onboarding SHA, not a moving main. + +```sh +cd /home/ts/wt/openhcs-custom-function-admission-20260929 +env OPENHCS_CPU_ONLY=true PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH=/home/ts/wt/openhcs-custom-function-admission-20260929:/home/ts/wt/openhcs-custom-function-admission-20260929/external/ObjectState/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/arraybridge/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/metaclass-registry/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pycodify/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/pyqt-reactive/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/python-introspect/src:/home/ts/wt/openhcs-custom-function-admission-20260929/external/zmqruntime/src \ + POLYSTORE_IMAGEJ_CACHE_ROOT=/home/ts/.cache/polystore/imagej \ + POLYSTORE_IMAGEJ_ALLOW_DOWNLOAD=false \ + /home/ts/code/projects/openhcs/.venv/bin/python -B \ + tests/diagnostics/check_custom_registration_live.py \ + --released-runtime-slot --expected-source-sha FROZEN_HEAD \ + --receipts tests/runtime_diagnostics/registration_live_20260929_attempt03 \ + --scratch /home/ts/.cache/agent-scratch/openhcs-registration-live-230-20260929-attempt03 \ + --port 15997 +``` + +Ports15997/15998/15999 must be absent before launch (driver checks). Three headless +owned/foreign/unsupported synthetic fixture servers only, threads1, scratch<80MiB, +no GUI/JVM/download/install/science/shared7777. Total journey240s, each observation10s, +no redundant100s preparation cap. Existing typed start/status/stale-cancel observe +one future; exact audit requires zero source-bearing registration before READY, +exactly two total (initial plus controlled delayed reply), unchanged escaping-path +sentinels, then ordinary register/discover/compile/execute8x8 plus-three and same- +handle uncertainty reconciliation. Added onboarding calls are read-only and precede +first catalogue search. Captures and stage timing persist before disposition. + +On unexpected failure, retain original inputs/handles; inspect the same owner, +then use diagnostic `close-owned` only for supported identity-verified cleanup. +Do not restart/replay or inflate timeouts. On terminal, verify all recorded PIDs +absent and lock released before edits. Acceptance remains source-live, not installed. + +Later installed proof uses this same driver with --installed-entrypoint, PYTHONPATH +unset and cwd outside this source; ordinary McpDevServerSpec validates actual installed +OpenHCS/PolyStore paths. Parent owns that journey after reviewed merge/install. +Multiprocessing/cold hydration profiling is separate follow-up scope, not this gate. diff --git a/openhcs/agent/services/llm_context_service.py b/openhcs/agent/services/llm_context_service.py index 4c0ae9fc8b..adcd4f4d19 100644 --- a/openhcs/agent/services/llm_context_service.py +++ b/openhcs/agent/services/llm_context_service.py @@ -111,6 +111,26 @@ def render_authoring_context_sections( return (header, *sections, deepening) +class CatalogPreparationReadinessSection( + AuthoringContextSection, FirstUseWorkflowContext, PipelineAuthoringRulesContext +): + """One readiness entrypoint shared by first-use and pipeline contexts.""" + + section_id = "catalog_preparation_readiness" + + @classmethod + def render(cls, service: AgentAuthoringContextService) -> str: + del service + return f"""=== COLD LOCAL CATALOGUE READINESS === +Before first cold {agent_capabilities.search_functions.name}, discover "catalog preparation" +with {agent_capabilities.search_capabilities.name}. If exposed, follow +`openhcs_custom_function_workflow` / "Register on the intended process owner": +{agent_capabilities.start_function_catalog_preparation.name} on the intended existing endpoint, +then observe that exact handle until READY. Keep cold warming separate from +10-second tool observations; a timeout does not authorise restart/replay or +mutation. If not exposed, report the surface boundary rather than guess a route.""" + + class PipelineSystemModelSection( AuthoringContextSection, PipelineSystemModelContext, diff --git a/packaging/codex/openhcs/skills/use-openhcs/SKILL.md b/packaging/codex/openhcs/skills/use-openhcs/SKILL.md index 0a69897198..dd703ea50f 100644 --- a/packaging/codex/openhcs/skills/use-openhcs/SKILL.md +++ b/packaging/codex/openhcs/skills/use-openhcs/SKILL.md @@ -9,6 +9,7 @@ description: Operate local OpenHCS microscopy workflows through the bundled MCP 2. Call `openhcs_get_authoring_context` with `kind="first_use"` when choosing or resuming an OpenHCS workflow, then request exactly the task context it names. For multisite assembly, registration, normalisation, segmentation review, or image-result QC, `kind="image_analysis_workflow"` is the canonical operating guide; follow it rather than reconstructing those rules from this skill. 3. Treat Python pipeline source as one complete `PipelineDocument`: `pipeline_config` plus ordered `pipeline_steps`. Never send a steps-only fragment, mirror configuration through a second argument, or strip source bindings from the reviewed document. UI-visible and headless routes use these same declarations even though their process and state owners differ. 4. Call `openhcs_search_capabilities` with task-relevant workflow, target, or text filters. Its current `surface_profile`, registry-owned workflow metadata, side effects, and security metadata—not a remembered tool list—decide whether to use a UI-visible or exposed headless route. Use `openhcs_list_capabilities` only when the complete selected surface is required. + Before the first cold local function-catalogue search, discover `catalog preparation` capabilities and follow the existing [responsive preparation procedure](references/custom-function-authoring.md#register-on-the-intended-process-owner) (`openhcs_custom_function_workflow`) on the intended existing endpoint. Observe its exact handle until READY; keep cold warming separate from ordinary 10-second tool observations. A timeout is not permission to restart or replay. 5. Start with validated OpenHCS/CellProfiler benchmark examples, especially the first-class Official30 corpus. Search the biological task plus `OpenHCS Python`, retrieve the exact `openhcs_official30_benchmark_recipes` section with `max_chars=50000`, and inspect its native reference, pipeline, input/channel assumptions, settings, and case-specific parity evidence. Benchmark parity is meaningful validation for that reference scope; it is not biological raw/overlay acceptance for a different assay. Record the example source and evidence tier before adapting it. CellProfiler image, object, measurement, relationship, and export semantics lower into the same OpenHCS declarations and runtime. For a compact, structured example of those fields, retrieve `openhcs_official30_examplehuman_nuclei_recipe_card` when the task involves ExampleHuman-style nuclei segmentation; its selected-value parity and unassessed new-assay QA are deliberately separate. Before authoring, retain the example-selection record described in [the autonomous analysis strategy](references/analysis-strategy.md) (`openhcs_autonomous_analysis_strategy`); reading this instruction is not evidence of retrieving a pipeline. For an unfamiliar assay or an unresolved biological failure, use that guide's task-specific routes to image interpretation, preprocessing, segmentation diagnostics, measurements and recipe/error learning. Retrieve only the relevant guide; infer a provisional strategy from acquisition and matched raw channels before asking the user for missing information. These guides adapt Agentic-J's domain knowledge, not its Fiji command strings or model defaults. diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 15f813d5af..2bf83d6d4a 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -292,6 +292,11 @@ def save(): store = CustomFunctionManager.default_storage_directory() from zmqruntime.messages import ExecutionStatus + from openhcs.agent.capabilities import ( + AgentCapabilitySearchResult, + agent_capabilities, + ) + from openhcs.agent.dto.authoring import AuthoringContext from openhcs.agent.dto.execution import ( ExecutionJobRef, ExecutionJobStatus, @@ -305,6 +310,7 @@ def save(): FunctionCatalogPreparationState, FunctionDetail, ) + from openhcs.agent.dto.knowledge import KnowledgeBaseDocument servers = [] handles = [] @@ -492,7 +498,34 @@ def finish_job(ref): ) receipt["mcp_pid"] = health["server_process_id"] receipt["mcp_launch_environment"] = client.server_spec.environment() - call("openhcs_get_authoring_context", {"kind": "first_use"}) + for kind in ("first_use", "pipeline"): + context = call( + agent_capabilities.get_authoring_context.name, + {"kind": kind}, + AuthoringContext, + ) + assert ( + agent_capabilities.start_function_catalog_preparation.name + in context.content + ) + guide = call( + agent_capabilities.get_knowledge_document.name, + {"document_id": "openhcs_custom_function_workflow"}, + KnowledgeBaseDocument, + ) + assert not guide.truncated + assert ( + agent_capabilities.start_function_catalog_preparation.name in guide.content + ) + discovery = call( + agent_capabilities.search_capabilities.name, + {"query": "catalog preparation", "limit": 10}, + AgentCapabilitySearchResult, + ) + assert agent_capabilities.start_function_catalog_preparation.name in { + capability.name for capability in discovery.capabilities + } + receipt["readiness_guidance_reachable"] = True call("openhcs_get_authoring_context", {"kind": "custom_function"}) call("openhcs_search_capabilities", {"query": "custom function", "limit": 5}) call( diff --git a/tests/runtime_diagnostics/registration_main240_build_20260929/build.txt b/tests/runtime_diagnostics/registration_main240_build_20260929/build.txt new file mode 100644 index 0000000000..19dd501614 --- /dev/null +++ b/tests/runtime_diagnostics/registration_main240_build_20260929/build.txt @@ -0,0 +1,9 @@ +running build_ext +building 'openhcs.processing.backends.cellprofiler._granularity_reconstruct' extension +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -fPIC -DPy_LIMITED_API=0x030B0000 -I/home/ts/code/projects/openhcs/.venv/include -I/home/ts/.pyenv/versions/3.12.3/include/python3.12 -c openhcs/processing/backends/cellprofiler/_granularity_reconstruct.cpp -o /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/objects/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.o -O3 +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -shared /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/objects/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.o -L/home/ts/.pyenv/versions/3.12.3/lib -o /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/lib/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so +building 'openhcs.core._tabular_native' extension +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -fPIC -DPy_LIMITED_API=0x030B0000 -I/home/ts/code/projects/openhcs/.venv/include -I/home/ts/.pyenv/versions/3.12.3/include/python3.12 -c openhcs/core/_tabular_native.cpp -o /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/objects/openhcs/core/_tabular_native.o -O3 +g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -shared /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/objects/openhcs/core/_tabular_native.o -L/home/ts/.pyenv/versions/3.12.3/lib -o /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/lib/openhcs/core/_tabular_native.abi3.so +copying /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/lib/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so -> openhcs/processing/backends/cellprofiler +copying /home/ts/.cache/agent-scratch/openhcs-registration-main240-build-20260929/lib/openhcs/core/_tabular_native.abi3.so -> openhcs/core diff --git a/tests/runtime_diagnostics/registration_main240_build_20260929/imports.txt b/tests/runtime_diagnostics/registration_main240_build_20260929/imports.txt new file mode 100644 index 0000000000..27ceccaf54 --- /dev/null +++ b/tests/runtime_diagnostics/registration_main240_build_20260929/imports.txt @@ -0,0 +1,19 @@ +{ + "imports": { + "openhcs": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py", + "polystore": "/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py" + }, + "extensions": [ + { + "path": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/processing/backends/cellprofiler/_granularity_reconstruct.abi3.so", + "bytes": 45440, + "sha256": "f0627903f263f701fc50736a934ee1b727ddcf4cbd97375f9933cf1c9182c091" + }, + { + "path": "/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/core/_tabular_native.abi3.so", + "bytes": 210808, + "sha256": "d0051154f8af59874004373603aabff0ae0576216c48076e1470d6569bb18b88" + } + ], + "scope": "source-only standard build/import, no install, kernel warming or JVM" +} diff --git a/tests/runtime_diagnostics/registration_main240_build_20260929/resources.txt b/tests/runtime_diagnostics/registration_main240_build_20260929/resources.txt new file mode 100644 index 0000000000..f379751c92 --- /dev/null +++ b/tests/runtime_diagnostics/registration_main240_build_20260929/resources.txt @@ -0,0 +1 @@ +elapsed=2.55 max_rss_kib=146140 exit=0 diff --git a/tests/runtime_diagnostics/registration_main240_tests_20260929/pytest.txt b/tests/runtime_diagnostics/registration_main240_tests_20260929/pytest.txt new file mode 100644 index 0000000000..f27d72cc6b --- /dev/null +++ b/tests/runtime_diagnostics/registration_main240_tests_20260929/pytest.txt @@ -0,0 +1,16 @@ +openhcs=/home/ts/wt/openhcs-custom-function-admission-20260929/openhcs/__init__.py +polystore=/home/ts/wt/openhcs-custom-function-admission-20260929/external/PolyStore/src/polystore/__init__.py +................ [100%] +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +16 passed, 2 warnings in 5.53s diff --git a/tests/runtime_diagnostics/registration_main240_tests_20260929/resources.txt b/tests/runtime_diagnostics/registration_main240_tests_20260929/resources.txt new file mode 100644 index 0000000000..e0eb7b7d3e --- /dev/null +++ b/tests/runtime_diagnostics/registration_main240_tests_20260929/resources.txt @@ -0,0 +1 @@ +elapsed=6.41 max_rss_kib=293380 exit=0 diff --git a/tests/runtime_diagnostics/registration_onboarding_20260929/pytest.txt b/tests/runtime_diagnostics/registration_onboarding_20260929/pytest.txt new file mode 100644 index 0000000000..6c95a33020 --- /dev/null +++ b/tests/runtime_diagnostics/registration_onboarding_20260929/pytest.txt @@ -0,0 +1,14 @@ +.......... [100%] +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +10 passed, 2 deselected, 2 warnings in 8.20s diff --git a/tests/runtime_diagnostics/registration_onboarding_20260929/pytest_initial.txt b/tests/runtime_diagnostics/registration_onboarding_20260929/pytest_initial.txt new file mode 100644 index 0000000000..3e18fb92f1 --- /dev/null +++ b/tests/runtime_diagnostics/registration_onboarding_20260929/pytest_initial.txt @@ -0,0 +1,83 @@ +..FFF.F..... [100%] +=================================== FAILURES =================================== +________ test_actual_source_backed_guides_fit_the_public_default_bound _________ + + def test_actual_source_backed_guides_fit_the_public_default_bound() -> None: + service = AgentAuthoringContextService( + function_catalog=_UnexpectedFunctionCatalog() + ) + for declaration in AuthoringContextDeclaration.__registry__.values(): + request = AuthoringContextRequest(kind=declaration.require_kind()) + complete = service.get_authoring_context(request.kind) + bounded = service.get_bounded_authoring_context(request) +> assert len(complete.content) <= request.max_chars +E assert 17387 <= 16000 +E + where 17387 = len('=== DESIGN AND VALIDATE AN IMAGE-ANALYSIS WORKFLOW ===\nUse this context when an analysis needs multisite assembly, r...pect images in Napari and Fiji, image browser streaming, hyperstacks, viewer instance management, and troubleshooting.') +E + where '=== DESIGN AND VALIDATE AN IMAGE-ANALYSIS WORKFLOW ===\nUse this context when an analysis needs multisite assembly, r...pect images in Napari and Fiji, image browser streaming, hyperstacks, viewer instance management, and troubleshooting.' = AuthoringContext(schema_version='openhcs.agent.v1', kind='image_analysis_workflow', content="=== DESIGN AND VALIDATE A...ect images in Napari and Fiji, image browser streaming, hyperstacks, viewer instance management, and troubleshooting.").content +E + and 16000 = AuthoringContextRequest(kind='image_analysis_workflow', max_chars=16000).max_chars + +tests/unit/agent/test_progressive_authoring_context.py:162: AssertionError +_ test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact[first_use] _ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fc67b71c500> +kind = 'first_use' + + @pytest.mark.parametrize("kind", ("first_use", "pipeline")) + def test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact( + monkeypatch, kind: str + ) -> None: +> from openhcs.agent.services.zmq_function_catalog_service import ( + ZMQFunctionCatalogService, + ) +E ModuleNotFoundError: No module named 'openhcs.agent.services.zmq_function_catalog_service' + +tests/unit/agent/test_progressive_authoring_context.py:170: ModuleNotFoundError +_ test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact[pipeline] _ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fc67b71eed0> +kind = 'pipeline' + + @pytest.mark.parametrize("kind", ("first_use", "pipeline")) + def test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact( + monkeypatch, kind: str + ) -> None: +> from openhcs.agent.services.zmq_function_catalog_service import ( + ZMQFunctionCatalogService, + ) +E ModuleNotFoundError: No module named 'openhcs.agent.services.zmq_function_catalog_service' + +tests/unit/agent/test_progressive_authoring_context.py:170: ModuleNotFoundError +_________ test_every_context_is_complete_within_the_progressive_bound __________ + + def test_every_context_is_complete_within_the_progressive_bound() -> None: + service, config_service = _service() + + for declaration in AuthoringContextDeclaration.__registry__.values(): + route = declaration.require_route() + context = service.get_authoring_context(declaration.require_kind()) + + assert context.content.startswith(f"=== {route.title.upper()} ===") +> assert len(context.content) < 16_000 +E assert 17087 < 16000 +E + where 17087 = len('=== DESIGN AND VALIDATE AN IMAGE-ANALYSIS WORKFLOW ===\nUse this context when an analysis needs multisite assembly, r...ng_semantics\n- openhcs_viewer_management — Title for openhcs_viewer_management: Summary for openhcs_viewer_management') +E + where '=== DESIGN AND VALIDATE AN IMAGE-ANALYSIS WORKFLOW ===\nUse this context when an analysis needs multisite assembly, r...ng_semantics\n- openhcs_viewer_management — Title for openhcs_viewer_management: Summary for openhcs_viewer_management' = AuthoringContext(schema_version='openhcs.agent.v1', kind='image_analysis_workflow', content="=== DESIGN AND VALIDATE A...g_semantics\n- openhcs_viewer_management — Title for openhcs_viewer_management: Summary for openhcs_viewer_management").content + +tests/unit/agent/test_progressive_authoring_context.py:258: AssertionError +=============================== warnings summary =============================== +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +../../code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441 + /home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1441: PytestConfigWarning: Unknown config option: asyncio_mode + + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +=========================== short test summary info ============================ +FAILED tests/unit/agent/test_progressive_authoring_context.py::test_actual_source_backed_guides_fit_the_public_default_bound +FAILED tests/unit/agent/test_progressive_authoring_context.py::test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact[first_use] +FAILED tests/unit/agent/test_progressive_authoring_context.py::test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact[pipeline] +FAILED tests/unit/agent/test_progressive_authoring_context.py::test_every_context_is_complete_within_the_progressive_bound +4 failed, 8 passed, 2 warnings in 8.29s diff --git a/tests/runtime_diagnostics/registration_onboarding_20260929/resources.txt b/tests/runtime_diagnostics/registration_onboarding_20260929/resources.txt new file mode 100644 index 0000000000..ccb159486f --- /dev/null +++ b/tests/runtime_diagnostics/registration_onboarding_20260929/resources.txt @@ -0,0 +1,23 @@ + Command being timed: "/home/ts/code/projects/openhcs/.venv/bin/python -B -m pytest --noconftest -o addopts= tests/unit/agent/test_progressive_authoring_context.py -k not actual_source_backed_guides_fit and not every_context_is_complete --basetemp=/home/ts/.cache/agent-scratch/openhcs-registration-onboarding-20260929/pytest_final -q" + User time (seconds): 8.34 + System time (seconds): 0.53 + Percent of CPU this job got: 98% + Elapsed (wall clock) time (h:mm:ss or m:ss): 0:09.03 + Average shared text size (kbytes): 0 + Average unshared data size (kbytes): 0 + Average stack size (kbytes): 0 + Average total size (kbytes): 0 + Maximum resident set size (kbytes): 308356 + Average resident set size (kbytes): 0 + Major (requiring I/O) page faults: 0 + Minor (reclaiming a frame) page faults: 94245 + Voluntary context switches: 2 + Involuntary context switches: 46 + Swaps: 0 + File system inputs: 0 + File system outputs: 32 + Socket messages sent: 0 + Socket messages received: 0 + Signals delivered: 0 + Page size (bytes): 4096 + Exit status: 0 diff --git a/tests/runtime_diagnostics/registration_onboarding_20260929/resources_initial.txt b/tests/runtime_diagnostics/registration_onboarding_20260929/resources_initial.txt new file mode 100644 index 0000000000..bd92aab33f --- /dev/null +++ b/tests/runtime_diagnostics/registration_onboarding_20260929/resources_initial.txt @@ -0,0 +1,24 @@ +Command exited with non-zero status 1 + Command being timed: "/home/ts/code/projects/openhcs/.venv/bin/python -B -m pytest --noconftest -o addopts= tests/unit/agent/test_progressive_authoring_context.py --basetemp=/home/ts/.cache/agent-scratch/openhcs-registration-onboarding-20260929/pytest -q" + User time (seconds): 8.47 + System time (seconds): 0.41 + Percent of CPU this job got: 99% + Elapsed (wall clock) time (h:mm:ss or m:ss): 0:08.95 + Average shared text size (kbytes): 0 + Average unshared data size (kbytes): 0 + Average stack size (kbytes): 0 + Average total size (kbytes): 0 + Maximum resident set size (kbytes): 277796 + Average resident set size (kbytes): 0 + Major (requiring I/O) page faults: 0 + Minor (reclaiming a frame) page faults: 76898 + Voluntary context switches: 27 + Involuntary context switches: 39 + Swaps: 0 + File system inputs: 1240 + File system outputs: 64 + Socket messages sent: 0 + Socket messages received: 0 + Signals delivered: 0 + Page size (bytes): 4096 + Exit status: 1 diff --git a/tests/runtime_diagnostics/registration_onboarding_20260929/scope.md b/tests/runtime_diagnostics/registration_onboarding_20260929/scope.md new file mode 100644 index 0000000000..dfad0787c5 --- /dev/null +++ b/tests/runtime_diagnostics/registration_onboarding_20260929/scope.md @@ -0,0 +1,42 @@ +# Source-only onboarding closure + +Existing Python and explicit reviewed OpenHCS/eight submodule source paths; +imports verified under this tree. Shared ImageJ cache/downloadfalse, threads1, +no GUI/JVM/native preparation, no install, no shared7777 contact. Nonblocking +validation.lock held during bounded pytest. Initial guard /home19.8GiB RAM13.1GiB +warned disk+swap: no new native journey started. + +Skill-creator quick_validate.py packaging/codex/openhcs/skills/use-openhcs: +`Skill is valid!`. Focused syntax/lint E9,F63,F7,F82 and git diff --check pass. +Full Ruff has pre-existing quoted-forward-annotation warnings in context service; +no blanket cleanup claimed or performed. + +Initial file run: 4 failed,8 passed in8.29s. Two new test failures were an incorrect +test import (zmq_function_catalog_service instead of endpoint_function_catalog_service), +corrected in place. Two unchanged broad checks fail on image_analysis_workflow +exceeding the public16000-character bound. Read-only process-local exclusion of +the new registered section proved lengths: + +| Context | With section | Baseline without section | +| --- | ---: | ---: | +| first_use | 6169 | 5308 | +| pipeline | 12505 | 11644 | +| image_analysis_workflow | 17387 | 17387 | + +These lengths were measured before shortening the new pointer. The unchanged +image-analysis overflow is not repaired by this scoped registration PR. The two +existing whole-context bound tests are explicitly deselected in the final shard, +not reported passing. Parent owns that separate guide integration boundary. + +Final source shard:10 passed,2 deselected,8.20s; elapsed9.03s peak308356KiB exit0. +Two parametrized real-default-context/generated-FastMCP cases fetch first_use / +pipeline, retrieve the complete canonical source-backed knowledge document, and +discover the typed start capability through normal search_capabilities. An +interception fails any endpoint-client creation, so this discovery is independent +of cold catalogue readiness. This is source reachability, not stdio/installed proof. + +One nominal section inherits existing FirstUseWorkflowContext and +PipelineAuthoringRulesContext facets; section membership/rendering uses the +existing registry. Capability names derive from declarations. No duplicated +preparation procedure, tool inventory, guide, endpoint resolver or biological +hint. Diagnostic next journey performs the same read-only checks before search. diff --git a/tests/unit/agent/test_progressive_authoring_context.py b/tests/unit/agent/test_progressive_authoring_context.py index 524f712a6e..2d1d060c23 100644 --- a/tests/unit/agent/test_progressive_authoring_context.py +++ b/tests/unit/agent/test_progressive_authoring_context.py @@ -1,7 +1,12 @@ from __future__ import annotations +import asyncio +import json from pathlib import Path +import pytest +from python_introspect import dataclass_from_mapping + from openhcs.agent.authoring_contexts import ( AuthoringContextDeclaration, AuthoringContextRoute, @@ -10,12 +15,17 @@ UiVisibleWorkflowAuthoringContext, ViewerReviewAuthoringContext, ) -from openhcs.agent.capabilities import CapabilityTransport, agent_capabilities -from openhcs.agent.dto.authoring import AuthoringContextRequest +from openhcs.agent.capabilities import ( + AgentCapabilitySearchResult, + CapabilityTransport, + agent_capabilities, +) +from openhcs.agent.dto.authoring import AuthoringContext, AuthoringContextRequest from openhcs.agent.dto.common import SCHEMA_VERSION from openhcs.agent.dto.config import ConfigFieldSchema, ConfigSchema from openhcs.agent.dto.knowledge import ( KnowledgeBaseCatalog, + KnowledgeBaseDocument, KnowledgeBaseDocumentSummary, KnowledgeBaseDocumentTarget, ) @@ -153,6 +163,70 @@ def test_actual_source_backed_guides_fit_the_public_default_bound() -> None: assert bounded.content == complete.content +@pytest.mark.parametrize("kind", ("first_use", "pipeline")) +def test_actual_mcp_onboarding_reaches_responsive_preparation_without_endpoint_contact( + monkeypatch, kind: str +) -> None: + from openhcs.agent.services.endpoint_function_catalog_service import ( + ZMQFunctionCatalogService, + ) + from openhcs.mcp.context import create_agent_context + from openhcs.mcp.server import build_server + + def reject_endpoint_contact(*_args, **_kwargs): + raise AssertionError( + "guide/capability discovery must not contact a catalogue endpoint" + ) + + monkeypatch.setattr( + ZMQFunctionCatalogService, "_new_client", reject_endpoint_contact + ) + context = create_agent_context() + + async def invoke(): + built = build_server(context) + + async def read(capability, arguments, result_type): + response = await built.call_tool(capability.name, arguments) + content = response[0] if isinstance(response, tuple) else response.content + payload = json.loads(content[0].text) + assert not payload.get("errors") + return dataclass_from_mapping(result_type, payload) + + onboarding = await read( + agent_capabilities.get_authoring_context, + {"kind": kind}, + AuthoringContext, + ) + assert ( + agent_capabilities.start_function_catalog_preparation.name + in onboarding.content + ) + guide = await read( + agent_capabilities.get_knowledge_document, + {"document_id": "openhcs_custom_function_workflow"}, + KnowledgeBaseDocument, + ) + assert guide.document.document_id == "openhcs_custom_function_workflow" + assert not guide.truncated + assert ( + agent_capabilities.start_function_catalog_preparation.name in guide.content + ) + discovery = await read( + agent_capabilities.search_capabilities, + {"query": "catalog preparation", "limit": 10}, + AgentCapabilitySearchResult, + ) + assert agent_capabilities.start_function_catalog_preparation.name in { + capability.name for capability in discovery.capabilities + } + + try: + asyncio.run(invoke()) + finally: + context.endpoint_function_catalog.close() + + def test_first_use_projects_new_routes_from_the_nominal_registry() -> None: class _TemporaryAuthoringContext(AuthoringContextDeclaration): kind = "temporary_progressive_test" From cf5bed3d27270ca9eca47fd378c6e64c6637e868 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 20:26:53 -0400 Subject: [PATCH 13/13] Construct native acceptance fixture with declared lazy config owners --- ...egistration_source_live_handoff_20260929.md | 18 ++++++++++++++++++ .../check_custom_registration_live.py | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/plans/custom_registration_source_live_handoff_20260929.md b/docs/plans/custom_registration_source_live_handoff_20260929.md index cf6d8e06d1..aecf2117c9 100644 --- a/docs/plans/custom_registration_source_live_handoff_20260929.md +++ b/docs/plans/custom_registration_source_live_handoff_20260929.md @@ -73,3 +73,21 @@ Later installed proof uses this same driver with --installed-entrypoint, PYTHONP unset and cwd outside this source; ordinary McpDevServerSpec validates actual installed OpenHCS/PolyStore paths. Parent owns that journey after reviewed merge/install. Multiprocessing/cold hydration profiling is separate follow-up scope, not this gate. + +## Parent-owned fixture correction after attempt03 + +Parent accepted the source handoff at82f265d3a. Attempt03 reached READY, verified +zero pre-ready source RPCs, registered/discovered/described the persisted synthetic +function (registration0.272s), then failed before compile while constructing the +fixture's PipelineConfig: VFSConfig is not its declared LazyVFSConfig field type. +The same declaration requires LazyPathPlanningConfig as well. Parent replaced +both fixture constructors with their actual declared owners, not converters or +production compatibility. A real source-import constructor check passes for +both nested fields. No compile/execution request was sent by this failed attempt. + +Original attempt03 receipt/scratch remain retained with accepted=false. Supported +close-owned completed all three exact native endpoints with returncode0 and +endpoint_terminated=true; client_closed/runtime_terminal=true. Recorded driver, +MCP and native PIDs are absent; six listeners are absent. No recorded request +is replayed. The next independent synthetic journey uses new ports/owned store, +fresh receipt/scratch and the fixture-correction commit as its source freeze. diff --git a/tests/diagnostics/check_custom_registration_live.py b/tests/diagnostics/check_custom_registration_live.py index 2bf83d6d4a..2551f51402 100644 --- a/tests/diagnostics/check_custom_registration_live.py +++ b/tests/diagnostics/check_custom_registration_live.py @@ -637,11 +637,11 @@ def finish_job(ref): == "openhcs.processing.custom_functions.registration_live_probe" ) from openhcs.core.config import ( + LazyPathPlanningConfig, LazyStepMaterializationConfig, + LazyVFSConfig, MaterializationBackend, - PathPlanningConfig, PipelineConfig, - VFSConfig, ) from openhcs.core.pipeline_document import PipelineDocumentAuthority from openhcs.core.steps.function_step import FunctionStep @@ -651,10 +651,10 @@ def finish_job(ref): pipeline_config=PipelineConfig( num_workers=1, use_threading=True, - path_planning_config=PathPlanningConfig( + path_planning_config=LazyPathPlanningConfig( global_output_folder=owned / "outputs" ), - vfs_config=VFSConfig( + vfs_config=LazyVFSConfig( materialization_backend=MaterializationBackend.DISK ), ),