comfy-env 0.0.64__py3-none-any.whl → 0.0.66__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. comfy_env/__init__.py +70 -122
  2. comfy_env/cli.py +78 -7
  3. comfy_env/config/__init__.py +19 -0
  4. comfy_env/config/parser.py +151 -0
  5. comfy_env/config/types.py +64 -0
  6. comfy_env/install.py +83 -361
  7. comfy_env/isolation/__init__.py +9 -0
  8. comfy_env/isolation/wrap.py +351 -0
  9. comfy_env/nodes.py +2 -2
  10. comfy_env/pixi/__init__.py +48 -0
  11. comfy_env/pixi/core.py +356 -0
  12. comfy_env/{resolver.py → pixi/resolver.py} +1 -14
  13. comfy_env/prestartup.py +60 -0
  14. comfy_env/templates/comfy-env-instructions.txt +30 -87
  15. comfy_env/templates/comfy-env.toml +68 -136
  16. comfy_env/workers/__init__.py +21 -32
  17. comfy_env/workers/base.py +1 -1
  18. comfy_env/workers/{torch_mp.py → mp.py} +47 -14
  19. comfy_env/workers/{venv.py → subprocess.py} +405 -441
  20. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
  21. comfy_env-0.0.66.dist-info/RECORD +34 -0
  22. comfy_env/decorator.py +0 -700
  23. comfy_env/env/__init__.py +0 -47
  24. comfy_env/env/config.py +0 -201
  25. comfy_env/env/config_file.py +0 -740
  26. comfy_env/env/manager.py +0 -636
  27. comfy_env/env/security.py +0 -267
  28. comfy_env/ipc/__init__.py +0 -55
  29. comfy_env/ipc/bridge.py +0 -476
  30. comfy_env/ipc/protocol.py +0 -265
  31. comfy_env/ipc/tensor.py +0 -371
  32. comfy_env/ipc/torch_bridge.py +0 -401
  33. comfy_env/ipc/transport.py +0 -318
  34. comfy_env/ipc/worker.py +0 -221
  35. comfy_env/isolation.py +0 -310
  36. comfy_env/pixi.py +0 -760
  37. comfy_env/stub_imports.py +0 -270
  38. comfy_env/stubs/__init__.py +0 -1
  39. comfy_env/stubs/comfy/__init__.py +0 -6
  40. comfy_env/stubs/comfy/model_management.py +0 -58
  41. comfy_env/stubs/comfy/utils.py +0 -29
  42. comfy_env/stubs/folder_paths.py +0 -71
  43. comfy_env/workers/pool.py +0 -241
  44. comfy_env-0.0.64.dist-info/RECORD +0 -48
  45. /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
  46. /comfy_env/{env → pixi}/platform/__init__.py +0 -0
  47. /comfy_env/{env → pixi}/platform/base.py +0 -0
  48. /comfy_env/{env → pixi}/platform/darwin.py +0 -0
  49. /comfy_env/{env → pixi}/platform/linux.py +0 -0
  50. /comfy_env/{env → pixi}/platform/windows.py +0 -0
  51. /comfy_env/{registry.py → pixi/registry.py} +0 -0
  52. /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
  53. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
  54. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
  55. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,11 @@
1
1
  """
2
- VenvWorker - Cross-venv isolation using subprocess + shared memory.
2
+ SubprocessWorker - Cross-venv isolation using persistent subprocess + socket IPC.
3
3
 
4
4
  This worker supports calling functions in a different Python environment:
5
- - Uses subprocess.Popen to run in different venv
6
- - Transfers tensors via torch.save/load through /dev/shm (RAM-backed)
7
- - One memcpy per tensor per direction
8
- - ~100-500ms overhead per call (subprocess spawn + tensor I/O)
5
+ - Uses a persistent subprocess to avoid spawn overhead
6
+ - Socket-based IPC for commands/responses
7
+ - Transfers tensors via torch.save/load over socket
8
+ - ~50-100ms overhead per call
9
9
 
10
10
  Use this when you need:
11
11
  - Different PyTorch version
@@ -13,16 +13,17 @@ Use this when you need:
13
13
  - Different Python version
14
14
 
15
15
  Example:
16
- worker = VenvWorker(
16
+ worker = SubprocessWorker(
17
17
  python="/path/to/other/venv/bin/python",
18
18
  working_dir="/path/to/code",
19
19
  )
20
20
 
21
- # Call a function by module path
22
- result = worker.call_module(
23
- module="my_module",
24
- func="my_function",
25
- image=my_tensor,
21
+ # Call a method by module path
22
+ result = worker.call_method(
23
+ module_name="my_module",
24
+ class_name="MyClass",
25
+ method_name="process",
26
+ kwargs={"image": my_tensor},
26
27
  )
27
28
  """
28
29
 
@@ -185,7 +186,142 @@ class SocketTransport:
185
186
 
186
187
 
187
188
  # =============================================================================
188
- # Serialization helpers
189
+ # Shared Memory Serialization
190
+ # =============================================================================
191
+
192
+ from multiprocessing import shared_memory as shm
193
+ import numpy as np
194
+
195
+
196
+ def _to_shm(obj, registry, visited=None):
197
+ """
198
+ Serialize object to shared memory. Returns JSON-safe metadata.
199
+
200
+ Args:
201
+ obj: Object to serialize
202
+ registry: List to track SharedMemory objects for cleanup
203
+ visited: Dict tracking already-serialized objects (cycle detection)
204
+ """
205
+ if visited is None:
206
+ visited = {}
207
+
208
+ obj_id = id(obj)
209
+ if obj_id in visited:
210
+ return visited[obj_id]
211
+
212
+ t = type(obj).__name__
213
+
214
+ # numpy array → direct shared memory
215
+ if t == 'ndarray':
216
+ arr = np.ascontiguousarray(obj)
217
+ block = shm.SharedMemory(create=True, size=arr.nbytes)
218
+ np.ndarray(arr.shape, arr.dtype, buffer=block.buf)[:] = arr
219
+ registry.append(block)
220
+ result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
221
+ visited[obj_id] = result
222
+ return result
223
+
224
+ # torch.Tensor → convert to numpy → shared memory
225
+ if t == 'Tensor':
226
+ arr = obj.detach().cpu().numpy()
227
+ return _to_shm(arr, registry, visited)
228
+
229
+ # trimesh.Trimesh → vertices + faces arrays → shared memory
230
+ if t == 'Trimesh':
231
+ verts = np.ascontiguousarray(obj.vertices, dtype=np.float64)
232
+ faces = np.ascontiguousarray(obj.faces, dtype=np.int64)
233
+
234
+ v_block = shm.SharedMemory(create=True, size=verts.nbytes)
235
+ np.ndarray(verts.shape, verts.dtype, buffer=v_block.buf)[:] = verts
236
+ registry.append(v_block)
237
+
238
+ f_block = shm.SharedMemory(create=True, size=faces.nbytes)
239
+ np.ndarray(faces.shape, faces.dtype, buffer=f_block.buf)[:] = faces
240
+ registry.append(f_block)
241
+
242
+ result = {
243
+ "__shm_trimesh__": True,
244
+ "v_name": v_block.name, "v_shape": list(verts.shape),
245
+ "f_name": f_block.name, "f_shape": list(faces.shape),
246
+ }
247
+ visited[obj_id] = result
248
+ return result
249
+
250
+ # Path → string
251
+ from pathlib import PurePath
252
+ if isinstance(obj, PurePath):
253
+ return str(obj)
254
+
255
+ # dict
256
+ if isinstance(obj, dict):
257
+ result = {k: _to_shm(v, registry, visited) for k, v in obj.items()}
258
+ visited[obj_id] = result
259
+ return result
260
+
261
+ # list/tuple
262
+ if isinstance(obj, list):
263
+ result = [_to_shm(v, registry, visited) for v in obj]
264
+ visited[obj_id] = result
265
+ return result
266
+ if isinstance(obj, tuple):
267
+ result = [_to_shm(v, registry, visited) for v in obj] # JSON doesn't have tuples
268
+ visited[obj_id] = result
269
+ return result
270
+
271
+ # primitives pass through
272
+ return obj
273
+
274
+
275
+ def _from_shm(obj, unlink=True):
276
+ """Reconstruct object from shared memory metadata."""
277
+ if not isinstance(obj, dict):
278
+ if isinstance(obj, list):
279
+ return [_from_shm(v, unlink) for v in obj]
280
+ return obj
281
+
282
+ # numpy array
283
+ if "__shm_np__" in obj:
284
+ block = shm.SharedMemory(name=obj["__shm_np__"])
285
+ arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
286
+ block.close()
287
+ if unlink:
288
+ block.unlink()
289
+ return arr
290
+
291
+ # trimesh
292
+ if "__shm_trimesh__" in obj:
293
+ import trimesh
294
+ v_block = shm.SharedMemory(name=obj["v_name"])
295
+ verts = np.ndarray(tuple(obj["v_shape"]), dtype=np.float64, buffer=v_block.buf).copy()
296
+ v_block.close()
297
+ if unlink:
298
+ v_block.unlink()
299
+
300
+ f_block = shm.SharedMemory(name=obj["f_name"])
301
+ faces = np.ndarray(tuple(obj["f_shape"]), dtype=np.int64, buffer=f_block.buf).copy()
302
+ f_block.close()
303
+ if unlink:
304
+ f_block.unlink()
305
+
306
+ return trimesh.Trimesh(vertices=verts, faces=faces, process=False)
307
+
308
+ # regular dict - recurse
309
+ return {k: _from_shm(v, unlink) for k, v in obj.items()}
310
+
311
+
312
+ def _cleanup_shm(registry):
313
+ """Unlink all shared memory blocks in registry."""
314
+ for block in registry:
315
+ try:
316
+ block.close()
317
+ block.unlink()
318
+ except Exception:
319
+ pass
320
+ registry.clear()
321
+
322
+
323
+ # =============================================================================
324
+ # Legacy Serialization helpers (for isolated objects)
189
325
  # =============================================================================
190
326
 
191
327
 
@@ -204,6 +340,11 @@ def _serialize_for_ipc(obj, visited=None):
204
340
  if obj_id in visited:
205
341
  return visited[obj_id] # Return cached serialized result
206
342
 
343
+ # Handle Path objects - convert to string for JSON serialization
344
+ from pathlib import PurePath
345
+ if isinstance(obj, PurePath):
346
+ return str(obj)
347
+
207
348
  # Check if this is a custom object with broken module path
208
349
  if (hasattr(obj, '__dict__') and
209
350
  hasattr(obj, '__class__') and
@@ -252,104 +393,6 @@ def _serialize_for_ipc(obj, visited=None):
252
393
  return obj
253
394
 
254
395
 
255
- # Worker script template - minimal, runs in target venv
256
- _WORKER_SCRIPT = '''
257
- import sys
258
- import os
259
- import json
260
- import traceback
261
- from types import SimpleNamespace
262
-
263
- # On Windows, add DLL directories for proper library loading
264
- if sys.platform == "win32" and hasattr(os, "add_dll_directory"):
265
- _host_python_dir = os.environ.get("COMFYUI_HOST_PYTHON_DIR")
266
- if _host_python_dir:
267
- try:
268
- os.add_dll_directory(_host_python_dir)
269
- _dlls_dir = os.path.join(_host_python_dir, "DLLs")
270
- if os.path.isdir(_dlls_dir):
271
- os.add_dll_directory(_dlls_dir)
272
- except Exception:
273
- pass
274
- _pixi_library_bin = os.environ.get("COMFYUI_PIXI_LIBRARY_BIN")
275
- if _pixi_library_bin:
276
- try:
277
- os.add_dll_directory(_pixi_library_bin)
278
- except Exception:
279
- pass
280
-
281
- def _deserialize_isolated_objects(obj):
282
- """Reconstruct objects serialized with __isolated_object__ marker."""
283
- if isinstance(obj, dict):
284
- if obj.get("__isolated_object__"):
285
- # Reconstruct as SimpleNamespace (supports .attr access)
286
- attrs = {k: _deserialize_isolated_objects(v) for k, v in obj.get("__attrs__", {}).items()}
287
- ns = SimpleNamespace(**attrs)
288
- ns.__class_name__ = obj.get("__class_name__", "Unknown")
289
- return ns
290
- return {k: _deserialize_isolated_objects(v) for k, v in obj.items()}
291
- elif isinstance(obj, list):
292
- return [_deserialize_isolated_objects(v) for v in obj]
293
- elif isinstance(obj, tuple):
294
- return tuple(_deserialize_isolated_objects(v) for v in obj)
295
- return obj
296
-
297
- def main():
298
- # Read request from file
299
- request_path = sys.argv[1]
300
- response_path = sys.argv[2]
301
-
302
- with open(request_path, 'r') as f:
303
- request = json.load(f)
304
-
305
- try:
306
- # Setup paths
307
- for p in request.get("sys_path", []):
308
- if p not in sys.path:
309
- sys.path.insert(0, p)
310
-
311
- # Import torch for tensor I/O
312
- import torch
313
-
314
- # Load inputs
315
- inputs_path = request.get("inputs_path")
316
- if inputs_path:
317
- inputs = torch.load(inputs_path, weights_only=False)
318
- inputs = _deserialize_isolated_objects(inputs)
319
- else:
320
- inputs = {}
321
-
322
- # Import and call function
323
- module_name = request["module"]
324
- func_name = request["func"]
325
-
326
- module = __import__(module_name, fromlist=[func_name])
327
- func = getattr(module, func_name)
328
-
329
- result = func(**inputs)
330
-
331
- # Save outputs
332
- outputs_path = request.get("outputs_path")
333
- if outputs_path:
334
- torch.save(result, outputs_path)
335
-
336
- response = {"status": "ok"}
337
-
338
- except Exception as e:
339
- response = {
340
- "status": "error",
341
- "error": str(e),
342
- "traceback": traceback.format_exc(),
343
- }
344
-
345
- with open(response_path, 'w') as f:
346
- json.dump(response, f)
347
-
348
- if __name__ == "__main__":
349
- main()
350
- '''
351
-
352
-
353
396
  def _get_shm_dir() -> Path:
354
397
  """Get shared memory directory for efficient tensor transfer."""
355
398
  # Linux: /dev/shm is RAM-backed tmpfs
@@ -359,249 +402,6 @@ def _get_shm_dir() -> Path:
359
402
  return Path(tempfile.gettempdir())
360
403
 
361
404
 
362
- class VenvWorker(Worker):
363
- """
364
- Worker using subprocess for cross-venv isolation.
365
-
366
- This worker spawns a new Python process for each call, using
367
- a different Python interpreter (from another venv). Tensors are
368
- transferred via torch.save/load through shared memory.
369
-
370
- For long-running workloads, consider using persistent mode which
371
- keeps the subprocess alive between calls.
372
- """
373
-
374
- def __init__(
375
- self,
376
- python: Union[str, Path],
377
- working_dir: Optional[Union[str, Path]] = None,
378
- sys_path: Optional[List[str]] = None,
379
- env: Optional[Dict[str, str]] = None,
380
- name: Optional[str] = None,
381
- persistent: bool = True,
382
- ):
383
- """
384
- Initialize the worker.
385
-
386
- Args:
387
- python: Path to Python executable in target venv.
388
- working_dir: Working directory for subprocess.
389
- sys_path: Additional paths to add to sys.path in subprocess.
390
- env: Additional environment variables.
391
- name: Optional name for logging.
392
- persistent: If True, keep subprocess alive between calls (faster).
393
- """
394
- self.python = Path(python)
395
- self.working_dir = Path(working_dir) if working_dir else Path.cwd()
396
- self.sys_path = sys_path or []
397
- self.extra_env = env or {}
398
- self.name = name or f"VenvWorker({self.python.parent.parent.name})"
399
- self.persistent = persistent
400
-
401
- # Verify Python exists
402
- if not self.python.exists():
403
- raise FileNotFoundError(f"Python not found: {self.python}")
404
-
405
- # Create temp directory for IPC files
406
- self._temp_dir = Path(tempfile.mkdtemp(prefix='comfyui_venv_'))
407
- self._shm_dir = _get_shm_dir()
408
-
409
- # Persistent process state
410
- self._process: Optional[subprocess.Popen] = None
411
- self._shutdown = False
412
-
413
- # Write worker script
414
- self._worker_script = self._temp_dir / "worker.py"
415
- self._worker_script.write_text(_WORKER_SCRIPT)
416
-
417
- def call(
418
- self,
419
- func: Callable,
420
- *args,
421
- timeout: Optional[float] = None,
422
- **kwargs
423
- ) -> Any:
424
- """
425
- Execute a function - NOT SUPPORTED for VenvWorker.
426
-
427
- VenvWorker cannot pickle arbitrary functions across venv boundaries.
428
- Use call_module() instead to call functions by module path.
429
-
430
- Raises:
431
- NotImplementedError: Always.
432
- """
433
- raise NotImplementedError(
434
- f"{self.name}: VenvWorker cannot call arbitrary functions. "
435
- f"Use call_module(module='...', func='...', **kwargs) instead."
436
- )
437
-
438
- def call_module(
439
- self,
440
- module: str,
441
- func: str,
442
- timeout: Optional[float] = None,
443
- **kwargs
444
- ) -> Any:
445
- """
446
- Call a function by module path in the isolated venv.
447
-
448
- Args:
449
- module: Module name (e.g., "my_package.my_module").
450
- func: Function name within the module.
451
- timeout: Timeout in seconds (None = 600s default).
452
- **kwargs: Keyword arguments passed to the function.
453
- Must be torch.save-compatible (tensors, dicts, etc.).
454
-
455
- Returns:
456
- Return value of module.func(**kwargs).
457
-
458
- Raises:
459
- WorkerError: If function raises an exception.
460
- TimeoutError: If execution exceeds timeout.
461
- """
462
- if self._shutdown:
463
- raise RuntimeError(f"{self.name}: Worker has been shut down")
464
-
465
- timeout = timeout or 600.0 # 10 minute default
466
-
467
- # Create unique ID for this call
468
- call_id = str(uuid.uuid4())[:8]
469
-
470
- # Paths for IPC (use shm for tensors, temp for json)
471
- inputs_path = self._shm_dir / f"comfyui_venv_{call_id}_in.pt"
472
- outputs_path = self._shm_dir / f"comfyui_venv_{call_id}_out.pt"
473
- request_path = self._temp_dir / f"request_{call_id}.json"
474
- response_path = self._temp_dir / f"response_{call_id}.json"
475
-
476
- try:
477
- # Save inputs via torch.save (handles tensors natively)
478
- # Serialize custom objects with broken __module__ paths first
479
- import torch
480
- if kwargs:
481
- serialized_kwargs = _serialize_for_ipc(kwargs)
482
- torch.save(serialized_kwargs, str(inputs_path))
483
-
484
- # Build request
485
- request = {
486
- "module": module,
487
- "func": func,
488
- "sys_path": [str(self.working_dir)] + self.sys_path,
489
- "inputs_path": str(inputs_path) if kwargs else None,
490
- "outputs_path": str(outputs_path),
491
- }
492
-
493
- request_path.write_text(json.dumps(request))
494
-
495
- # Build environment
496
- env = os.environ.copy()
497
- env.update(self.extra_env)
498
- env["COMFYUI_ISOLATION_WORKER"] = "1"
499
-
500
- # For conda/pixi environments, add lib dir to LD_LIBRARY_PATH (Linux)
501
- lib_dir = self.python.parent.parent / "lib"
502
- if lib_dir.is_dir():
503
- existing = env.get("LD_LIBRARY_PATH", "")
504
- env["LD_LIBRARY_PATH"] = f"{lib_dir}:{existing}" if existing else str(lib_dir)
505
-
506
- # On Windows, pass host Python directory and pixi Library/bin for DLL loading
507
- if sys.platform == "win32":
508
- env["COMFYUI_HOST_PYTHON_DIR"] = str(Path(sys.executable).parent)
509
-
510
- # For pixi environments with MKL, add Library/bin to PATH for DLL loading
511
- # Pixi has python.exe directly in env dir, not in Scripts/
512
- env_dir = self.python.parent
513
- library_bin = env_dir / "Library" / "bin"
514
- if library_bin.is_dir():
515
- existing_path = env.get("PATH", "")
516
- env["PATH"] = f"{env_dir};{library_bin};{existing_path}"
517
- env["COMFYUI_PIXI_LIBRARY_BIN"] = str(library_bin)
518
- # Allow duplicate OpenMP libraries (MKL's libiomp5md.dll + PyTorch's libomp.dll)
519
- env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
520
- # Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
521
- env["PYTHONIOENCODING"] = "utf-8"
522
-
523
- # Run subprocess
524
- cmd = [
525
- str(self.python),
526
- str(self._worker_script),
527
- str(request_path),
528
- str(response_path),
529
- ]
530
-
531
- process = subprocess.Popen(
532
- cmd,
533
- cwd=str(self.working_dir),
534
- env=env,
535
- stdout=subprocess.PIPE,
536
- stderr=subprocess.PIPE,
537
- )
538
-
539
- try:
540
- stdout, stderr = process.communicate(timeout=timeout)
541
- except subprocess.TimeoutExpired:
542
- process.kill()
543
- process.wait()
544
- raise TimeoutError(f"{self.name}: Call timed out after {timeout}s")
545
-
546
- # Check for process error
547
- if process.returncode != 0:
548
- raise WorkerError(
549
- f"Subprocess failed with code {process.returncode}",
550
- traceback=stderr.decode('utf-8', errors='replace'),
551
- )
552
-
553
- # Read response
554
- if not response_path.exists():
555
- raise WorkerError(
556
- f"No response file",
557
- traceback=stderr.decode('utf-8', errors='replace'),
558
- )
559
-
560
- response = json.loads(response_path.read_text())
561
-
562
- if response["status"] == "error":
563
- raise WorkerError(
564
- response.get("error", "Unknown error"),
565
- traceback=response.get("traceback"),
566
- )
567
-
568
- # Load result
569
- if outputs_path.exists():
570
- result = torch.load(str(outputs_path), weights_only=False)
571
- return result
572
- else:
573
- return None
574
-
575
- finally:
576
- # Cleanup IPC files
577
- for path in [inputs_path, outputs_path, request_path, response_path]:
578
- try:
579
- if path.exists():
580
- path.unlink()
581
- except:
582
- pass
583
-
584
- def shutdown(self) -> None:
585
- """Shut down the worker and clean up resources."""
586
- if self._shutdown:
587
- return
588
-
589
- self._shutdown = True
590
-
591
- # Clean up temp directory
592
- try:
593
- shutil.rmtree(self._temp_dir, ignore_errors=True)
594
- except:
595
- pass
596
-
597
- def is_alive(self) -> bool:
598
- """VenvWorker spawns fresh process per call, so always 'alive' if not shutdown."""
599
- return not self._shutdown
600
-
601
- def __repr__(self):
602
- return f"<VenvWorker name={self.name!r} python={self.python}>"
603
-
604
-
605
405
  # Persistent worker script - runs as __main__ in the venv Python subprocess
606
406
  # Uses Unix socket (or TCP localhost) for IPC - completely separate from stdout/stderr
607
407
  _PERSISTENT_WORKER_SCRIPT = '''
@@ -717,6 +517,99 @@ if sys.platform == "win32":
717
517
  except Exception as e:
718
518
  wlog(f"[worker] Failed to add pixi Library/bin: {e}")
719
519
 
520
+ # =============================================================================
521
+ # Shared Memory Serialization
522
+ # =============================================================================
523
+
524
+ from multiprocessing import shared_memory as shm
525
+ import numpy as np
526
+
527
+ def _to_shm(obj, registry, visited=None):
528
+ """Serialize to shared memory. Returns JSON-safe metadata."""
529
+ if visited is None:
530
+ visited = {}
531
+ obj_id = id(obj)
532
+ if obj_id in visited:
533
+ return visited[obj_id]
534
+ t = type(obj).__name__
535
+
536
+ if t == 'ndarray':
537
+ arr = np.ascontiguousarray(obj)
538
+ block = shm.SharedMemory(create=True, size=arr.nbytes)
539
+ np.ndarray(arr.shape, arr.dtype, buffer=block.buf)[:] = arr
540
+ registry.append(block)
541
+ result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
542
+ visited[obj_id] = result
543
+ return result
544
+
545
+ if t == 'Tensor':
546
+ arr = obj.detach().cpu().numpy()
547
+ return _to_shm(arr, registry, visited)
548
+
549
+ if t == 'Trimesh':
550
+ verts = np.ascontiguousarray(obj.vertices, dtype=np.float64)
551
+ faces = np.ascontiguousarray(obj.faces, dtype=np.int64)
552
+
553
+ v_block = shm.SharedMemory(create=True, size=verts.nbytes)
554
+ np.ndarray(verts.shape, verts.dtype, buffer=v_block.buf)[:] = verts
555
+ registry.append(v_block)
556
+
557
+ f_block = shm.SharedMemory(create=True, size=faces.nbytes)
558
+ np.ndarray(faces.shape, faces.dtype, buffer=f_block.buf)[:] = faces
559
+ registry.append(f_block)
560
+
561
+ result = {
562
+ "__shm_trimesh__": True,
563
+ "v_name": v_block.name, "v_shape": list(verts.shape),
564
+ "f_name": f_block.name, "f_shape": list(faces.shape),
565
+ }
566
+ visited[obj_id] = result
567
+ return result
568
+
569
+ if isinstance(obj, dict):
570
+ result = {k: _to_shm(v, registry, visited) for k, v in obj.items()}
571
+ visited[obj_id] = result
572
+ return result
573
+ if isinstance(obj, (list, tuple)):
574
+ result = [_to_shm(v, registry, visited) for v in obj]
575
+ visited[obj_id] = result
576
+ return result
577
+
578
+ return obj
579
+
580
+ def _from_shm(obj):
581
+ """Reconstruct from shared memory metadata. Does NOT unlink - caller handles that."""
582
+ if not isinstance(obj, dict):
583
+ if isinstance(obj, list):
584
+ return [_from_shm(v) for v in obj]
585
+ return obj
586
+ if "__shm_np__" in obj:
587
+ block = shm.SharedMemory(name=obj["__shm_np__"])
588
+ arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
589
+ block.close()
590
+ return arr
591
+ if "__shm_trimesh__" in obj:
592
+ import trimesh
593
+ v_block = shm.SharedMemory(name=obj["v_name"])
594
+ verts = np.ndarray(tuple(obj["v_shape"]), dtype=np.float64, buffer=v_block.buf).copy()
595
+ v_block.close()
596
+
597
+ f_block = shm.SharedMemory(name=obj["f_name"])
598
+ faces = np.ndarray(tuple(obj["f_shape"]), dtype=np.int64, buffer=f_block.buf).copy()
599
+ f_block.close()
600
+
601
+ return trimesh.Trimesh(vertices=verts, faces=faces, process=False)
602
+ return {k: _from_shm(v) for k, v in obj.items()}
603
+
604
+ def _cleanup_shm(registry):
605
+ for block in registry:
606
+ try:
607
+ block.close()
608
+ block.unlink()
609
+ except Exception:
610
+ pass
611
+ registry.clear()
612
+
720
613
  # =============================================================================
721
614
  # Object Reference System - keep complex objects in worker, pass refs to host
722
615
  # =============================================================================
@@ -762,9 +655,38 @@ def _should_use_reference(obj):
762
655
  # Dicts, lists, tuples - recurse into contents (don't ref the container)
763
656
  if isinstance(obj, (dict, list, tuple)):
764
657
  return False
765
- # Everything else (trimesh, custom classes) - pass by reference
658
+ # Trimesh - pass by value but needs special handling (see _prepare_trimesh_for_pickle)
659
+ if obj_type == 'Trimesh':
660
+ return False
661
+ # Everything else (custom classes) - pass by reference
766
662
  return True
767
663
 
664
+ def _prepare_trimesh_for_pickle(mesh):
665
+ """
666
+ Prepare a trimesh object for cross-Python-version pickling.
667
+
668
+ Trimesh attaches helper objects (ray tracer, proximity query) that may use
669
+ native extensions like embreex. These cause import errors when unpickling
670
+ on a system without those extensions. We strip them - they'll be recreated
671
+ lazily when needed.
672
+
673
+ Note: Do NOT strip _cache - trimesh needs it to function properly.
674
+ """
675
+ # Make a copy to avoid modifying the original
676
+ mesh = mesh.copy()
677
+
678
+ # Remove helper objects that may have unpickleable native code references
679
+ # These are lazily recreated on first access anyway
680
+ # Do NOT remove _cache - it's needed for trimesh to work
681
+ for attr in ('ray', '_ray', 'permutate', 'nearest'):
682
+ try:
683
+ delattr(mesh, attr)
684
+ except AttributeError:
685
+ pass
686
+
687
+ return mesh
688
+
689
+
768
690
  def _serialize_result(obj, visited=None):
769
691
  """Convert result for IPC - complex objects become references."""
770
692
  if visited is None:
@@ -783,6 +705,11 @@ def _serialize_result(obj, visited=None):
783
705
 
784
706
  visited.add(obj_id)
785
707
 
708
+ # Handle trimesh objects specially - strip unpickleable native extensions
709
+ obj_type = type(obj).__name__
710
+ if obj_type == 'Trimesh':
711
+ return _prepare_trimesh_for_pickle(obj)
712
+
786
713
  if isinstance(obj, dict):
787
714
  return {k: _serialize_result(v, visited) for k, v in obj.items()}
788
715
  if isinstance(obj, list):
@@ -791,7 +718,6 @@ def _serialize_result(obj, visited=None):
791
718
  return tuple(_serialize_result(v, visited) for v in obj)
792
719
 
793
720
  # Convert numpy scalars to Python primitives for JSON serialization
794
- obj_type = type(obj).__name__
795
721
  if obj_type in ('float16', 'float32', 'float64'):
796
722
  return float(obj)
797
723
  if obj_type in ('int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'):
@@ -910,10 +836,52 @@ def main():
910
836
  if p not in sys.path:
911
837
  sys.path.insert(0, p)
912
838
 
913
- # Import torch after path setup
914
- wlog("[worker] Importing torch...")
915
- import torch
916
- wlog(f"[worker] Torch imported: {torch.__version__}")
839
+ # Try to import torch (optional - not all isolated envs need it)
840
+ _HAS_TORCH = False
841
+ try:
842
+ import torch
843
+ _HAS_TORCH = True
844
+ wlog(f"[worker] Torch imported: {torch.__version__}")
845
+ except ImportError:
846
+ wlog("[worker] Torch not available, using pickle for serialization")
847
+
848
+ # Setup log forwarding to host
849
+ # This makes print() and logging statements in node code visible to the user
850
+ import builtins
851
+ import logging
852
+ _original_print = builtins.print
853
+
854
+ def _forwarded_print(*args, **kwargs):
855
+ """Forward print() calls to host via socket."""
856
+ # Build message from args
857
+ sep = kwargs.get('sep', ' ')
858
+ message = sep.join(str(a) for a in args)
859
+ # Send to host
860
+ try:
861
+ transport.send({"type": "log", "message": message})
862
+ except Exception:
863
+ pass # Don't fail if transport is closed
864
+ # Also log locally for debugging
865
+ wlog(f"[print] {message}")
866
+
867
+ builtins.print = _forwarded_print
868
+
869
+ # Also forward logging module output
870
+ class SocketLogHandler(logging.Handler):
871
+ def emit(self, record):
872
+ try:
873
+ msg = self.format(record)
874
+ transport.send({"type": "log", "message": msg})
875
+ wlog(f"[log] {msg}")
876
+ except Exception:
877
+ pass
878
+
879
+ # Add our handler to the root logger
880
+ _socket_handler = SocketLogHandler()
881
+ _socket_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
882
+ logging.root.addHandler(_socket_handler)
883
+
884
+ wlog("[worker] Print and logging forwarding enabled")
917
885
 
918
886
  # Signal ready
919
887
  transport.send({"status": "ready"})
@@ -942,23 +910,20 @@ def main():
942
910
  transport.send({"status": "pong"})
943
911
  continue
944
912
 
913
+ shm_registry = []
945
914
  try:
946
915
  request_type = request.get("type", "call_module")
947
916
  module_name = request["module"]
948
- inputs_path = request.get("inputs_path")
949
- outputs_path = request.get("outputs_path")
950
917
  wlog(f"[worker] Request: {request_type} {module_name}")
951
918
 
952
- # Load inputs
953
- if inputs_path:
954
- wlog(f"[worker] Loading inputs from {inputs_path}...")
955
- inputs = torch.load(inputs_path, weights_only=False)
956
- wlog(f"[worker] Deserializing isolated objects...")
919
+ # Load inputs from shared memory
920
+ kwargs_meta = request.get("kwargs")
921
+ if kwargs_meta:
922
+ wlog(f"[worker] Reconstructing inputs from shm...")
923
+ inputs = _from_shm(kwargs_meta)
957
924
  inputs = _deserialize_isolated_objects(inputs)
958
- # Resolve any object references from previous node calls
959
- wlog(f"[worker] Resolving object references...")
960
925
  inputs = _deserialize_input(inputs)
961
- wlog(f"[worker] Inputs ready: {list(inputs.keys())}")
926
+ wlog(f"[worker] Inputs ready: {list(inputs.keys()) if isinstance(inputs, dict) else type(inputs)}")
962
927
  else:
963
928
  inputs = {}
964
929
 
@@ -987,14 +952,17 @@ def main():
987
952
  func = getattr(module, func_name)
988
953
  result = func(**inputs)
989
954
 
990
- # Save result - convert complex objects to references
991
- if outputs_path:
992
- serialized_result = _serialize_result(result)
993
- torch.save(serialized_result, outputs_path)
955
+ # Serialize result to shared memory
956
+ wlog(f"[worker] Serializing result to shm...")
957
+ result_meta = _to_shm(result, shm_registry)
958
+ wlog(f"[worker] Created {len(shm_registry)} shm blocks for result")
994
959
 
995
- transport.send({"status": "ok"})
960
+ transport.send({"status": "ok", "result": result_meta})
961
+ # Note: don't cleanup shm_registry here - host needs to read it
996
962
 
997
963
  except Exception as e:
964
+ # Cleanup shm on error since host won't read it
965
+ _cleanup_shm(shm_registry)
998
966
  transport.send({
999
967
  "status": "error",
1000
968
  "error": str(e),
@@ -1008,9 +976,9 @@ if __name__ == "__main__":
1008
976
  '''
1009
977
 
1010
978
 
1011
- class PersistentVenvWorker(Worker):
979
+ class SubprocessWorker(Worker):
1012
980
  """
1013
- Persistent version of VenvWorker that keeps subprocess alive.
981
+ Cross-venv worker using persistent subprocess + socket IPC.
1014
982
 
1015
983
  Uses Unix domain sockets (or TCP localhost on older Windows) for IPC.
1016
984
  This completely separates IPC from stdout/stderr, so C libraries
@@ -1019,11 +987,11 @@ class PersistentVenvWorker(Worker):
1019
987
  Benefits:
1020
988
  - Works on Windows with different venv Python (full isolation)
1021
989
  - Compiled CUDA extensions load correctly in the venv
1022
- - ~50-100ms per call (vs ~300-500ms for VenvWorker per-call spawn)
990
+ - ~50-100ms per call (persistent subprocess avoids spawn overhead)
1023
991
  - Tensor transfer via shared memory files
1024
992
  - Immune to stdout pollution from C libraries
1025
993
 
1026
- Use this for high-frequency calls to isolated venvs.
994
+ Use this for calls to isolated venvs with different Python/dependencies.
1027
995
  """
1028
996
 
1029
997
  def __init__(
@@ -1050,7 +1018,7 @@ class PersistentVenvWorker(Worker):
1050
1018
  self.working_dir = Path(working_dir) if working_dir else Path.cwd()
1051
1019
  self.sys_path = sys_path or []
1052
1020
  self.extra_env = env or {}
1053
- self.name = name or f"PersistentVenvWorker({self.python.parent.parent.name})"
1021
+ self.name = name or f"SubprocessWorker({self.python.parent.parent.name})"
1054
1022
 
1055
1023
  if not self.python.exists():
1056
1024
  raise FileNotFoundError(f"Python not found: {self.python}")
@@ -1196,21 +1164,26 @@ class PersistentVenvWorker(Worker):
1196
1164
  # Use UTF-8 encoding for stdout/stderr to handle Unicode symbols
1197
1165
  env["PYTHONIOENCODING"] = "utf-8"
1198
1166
 
1199
- # Find ComfyUI base and set env var for folder_paths stub
1167
+ # Find ComfyUI base and add to sys_path for real folder_paths/comfy modules
1168
+ # This works because comfy.options.args_parsing=False by default, so folder_paths
1169
+ # auto-detects its base directory from __file__ location
1200
1170
  comfyui_base = self._find_comfyui_base()
1201
1171
  if comfyui_base:
1202
- env["COMFYUI_BASE"] = str(comfyui_base)
1172
+ env["COMFYUI_BASE"] = str(comfyui_base) # Keep for fallback/debugging
1203
1173
 
1204
- # Add stubs directory to sys_path for folder_paths etc.
1205
- stubs_dir = Path(__file__).parent.parent / "stubs"
1206
- all_sys_path = [str(stubs_dir), str(self.working_dir)] + self.sys_path
1174
+ # Build sys_path: ComfyUI first (for real modules), then working_dir, then extras
1175
+ all_sys_path = []
1176
+ if comfyui_base:
1177
+ all_sys_path.append(str(comfyui_base))
1178
+ all_sys_path.append(str(self.working_dir))
1179
+ all_sys_path.extend(self.sys_path)
1207
1180
 
1208
1181
  # Launch subprocess with the venv Python, passing socket address
1209
1182
  # For pixi environments, use "pixi run python" to get proper environment activation
1210
1183
  # (CONDA_PREFIX, Library paths, etc.) which fixes DLL loading issues with bpy
1211
1184
  is_pixi = '.pixi' in str(self.python)
1212
1185
  if _DEBUG:
1213
- print(f"[PersistentVenvWorker] is_pixi={is_pixi}, python={self.python}", flush=True)
1186
+ print(f"[SubprocessWorker] is_pixi={is_pixi}, python={self.python}", flush=True)
1214
1187
  if is_pixi:
1215
1188
  # Find pixi project root (parent of .pixi directory)
1216
1189
  pixi_project = self.python
@@ -1219,7 +1192,7 @@ class PersistentVenvWorker(Worker):
1219
1192
  pixi_project = pixi_project.parent # Go up from .pixi to project root
1220
1193
  pixi_toml = pixi_project / "pixi.toml"
1221
1194
  if _DEBUG:
1222
- print(f"[PersistentVenvWorker] pixi_toml={pixi_toml}, exists={pixi_toml.exists()}", flush=True)
1195
+ print(f"[SubprocessWorker] pixi_toml={pixi_toml}, exists={pixi_toml.exists()}", flush=True)
1223
1196
 
1224
1197
  if pixi_toml.exists():
1225
1198
  pixi_exe = get_pixi_path()
@@ -1246,15 +1219,15 @@ class PersistentVenvWorker(Worker):
1246
1219
  launch_env = env
1247
1220
 
1248
1221
  if _DEBUG:
1249
- print(f"[PersistentVenvWorker] launching cmd={cmd[:3]}...", flush=True)
1222
+ print(f"[SubprocessWorker] launching cmd={cmd[:3]}...", flush=True)
1250
1223
  if launch_env:
1251
1224
  path_sep = ";" if sys.platform == "win32" else ":"
1252
1225
  path_parts = launch_env.get("PATH", "").split(path_sep)
1253
- print(f"[PersistentVenvWorker] PATH has {len(path_parts)} entries:", flush=True)
1226
+ print(f"[SubprocessWorker] PATH has {len(path_parts)} entries:", flush=True)
1254
1227
  for i, p in enumerate(path_parts[:10]): # Show first 10
1255
- print(f"[PersistentVenvWorker] [{i}] {p}", flush=True)
1228
+ print(f"[SubprocessWorker] [{i}] {p}", flush=True)
1256
1229
  if len(path_parts) > 10:
1257
- print(f"[PersistentVenvWorker] ... and {len(path_parts) - 10} more", flush=True)
1230
+ print(f"[SubprocessWorker] ... and {len(path_parts) - 10} more", flush=True)
1258
1231
  self._process = subprocess.Popen(
1259
1232
  cmd,
1260
1233
  stdin=subprocess.DEVNULL,
@@ -1341,9 +1314,21 @@ class PersistentVenvWorker(Worker):
1341
1314
  # Send request
1342
1315
  self._transport.send(request)
1343
1316
 
1344
- # Read response with timeout
1317
+ # Read response with timeout, handling log messages along the way
1345
1318
  try:
1346
- response = self._transport.recv(timeout=timeout)
1319
+ while True:
1320
+ response = self._transport.recv(timeout=timeout)
1321
+ if response is None:
1322
+ break # Timeout
1323
+
1324
+ # Handle log messages from worker
1325
+ if response.get("type") == "log":
1326
+ msg = response.get("message", "")
1327
+ print(f"[worker:{self.name}] {msg}", file=sys.stderr, flush=True)
1328
+ continue # Keep waiting for actual response
1329
+
1330
+ # Got a real response
1331
+ break
1347
1332
  except ConnectionError as e:
1348
1333
  # Socket closed - check if worker process died
1349
1334
  self._shutdown = True
@@ -1406,49 +1391,43 @@ class PersistentVenvWorker(Worker):
1406
1391
  """
1407
1392
  import sys
1408
1393
  if _DEBUG:
1409
- print(f"[PersistentVenvWorker] call_method: {module_name}.{class_name}.{method_name}", file=sys.stderr, flush=True)
1394
+ print(f"[SubprocessWorker] call_method: {module_name}.{class_name}.{method_name}", file=sys.stderr, flush=True)
1410
1395
 
1411
1396
  with self._lock:
1412
1397
  if _DEBUG:
1413
- print(f"[PersistentVenvWorker] acquired lock, ensuring started...", file=sys.stderr, flush=True)
1398
+ print(f"[SubprocessWorker] acquired lock, ensuring started...", file=sys.stderr, flush=True)
1414
1399
  self._ensure_started()
1415
1400
  if _DEBUG:
1416
- print(f"[PersistentVenvWorker] worker started/confirmed", file=sys.stderr, flush=True)
1401
+ print(f"[SubprocessWorker] worker started/confirmed", file=sys.stderr, flush=True)
1417
1402
 
1418
1403
  timeout = timeout or 600.0
1419
- call_id = str(uuid.uuid4())[:8]
1420
-
1421
- import torch
1422
- inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
1423
- outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
1404
+ shm_registry = []
1424
1405
 
1425
1406
  try:
1426
- # Serialize kwargs
1407
+ # Serialize kwargs to shared memory
1427
1408
  if kwargs:
1428
1409
  if _DEBUG:
1429
- print(f"[PersistentVenvWorker] serializing kwargs...", file=sys.stderr, flush=True)
1430
- serialized_kwargs = _serialize_for_ipc(kwargs)
1431
- if _DEBUG:
1432
- print(f"[PersistentVenvWorker] saving to {inputs_path}...", file=sys.stderr, flush=True)
1433
- torch.save(serialized_kwargs, str(inputs_path))
1410
+ print(f"[SubprocessWorker] serializing kwargs to shm...", file=sys.stderr, flush=True)
1411
+ kwargs_meta = _to_shm(kwargs, shm_registry)
1434
1412
  if _DEBUG:
1435
- print(f"[PersistentVenvWorker] saved inputs", file=sys.stderr, flush=True)
1413
+ print(f"[SubprocessWorker] created {len(shm_registry)} shm blocks", file=sys.stderr, flush=True)
1414
+ else:
1415
+ kwargs_meta = None
1436
1416
 
1437
- # Send request with class info
1417
+ # Send request with shared memory metadata
1438
1418
  request = {
1439
1419
  "type": "call_method",
1440
1420
  "module": module_name,
1441
1421
  "class_name": class_name,
1442
1422
  "method_name": method_name,
1443
- "self_state": self_state,
1444
- "inputs_path": str(inputs_path) if kwargs else None,
1445
- "outputs_path": str(outputs_path),
1423
+ "self_state": _serialize_for_ipc(self_state) if self_state else None,
1424
+ "kwargs": kwargs_meta,
1446
1425
  }
1447
1426
  if _DEBUG:
1448
- print(f"[PersistentVenvWorker] sending request via socket...", file=sys.stderr, flush=True)
1427
+ print(f"[SubprocessWorker] sending request via socket...", file=sys.stderr, flush=True)
1449
1428
  response = self._send_request(request, timeout)
1450
1429
  if _DEBUG:
1451
- print(f"[PersistentVenvWorker] got response: {response.get('status')}", file=sys.stderr, flush=True)
1430
+ print(f"[SubprocessWorker] got response: {response.get('status')}", file=sys.stderr, flush=True)
1452
1431
 
1453
1432
  if response.get("status") == "error":
1454
1433
  raise WorkerError(
@@ -1456,16 +1435,14 @@ class PersistentVenvWorker(Worker):
1456
1435
  traceback=response.get("traceback"),
1457
1436
  )
1458
1437
 
1459
- if outputs_path.exists():
1460
- return torch.load(str(outputs_path), weights_only=False)
1438
+ # Reconstruct result from shared memory
1439
+ result_meta = response.get("result")
1440
+ if result_meta is not None:
1441
+ return _from_shm(result_meta)
1461
1442
  return None
1462
1443
 
1463
1444
  finally:
1464
- for p in [inputs_path, outputs_path]:
1465
- try:
1466
- p.unlink()
1467
- except:
1468
- pass
1445
+ _cleanup_shm(shm_registry)
1469
1446
 
1470
1447
  def call_module(
1471
1448
  self,
@@ -1479,25 +1456,16 @@ class PersistentVenvWorker(Worker):
1479
1456
  self._ensure_started()
1480
1457
 
1481
1458
  timeout = timeout or 600.0
1482
- call_id = str(uuid.uuid4())[:8]
1483
-
1484
- # Save inputs
1485
- import torch
1486
- inputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_in.pt"
1487
- outputs_path = self._shm_dir / f"comfyui_pvenv_{call_id}_out.pt"
1459
+ shm_registry = []
1488
1460
 
1489
1461
  try:
1490
- if kwargs:
1491
- serialized_kwargs = _serialize_for_ipc(kwargs)
1492
- torch.save(serialized_kwargs, str(inputs_path))
1462
+ kwargs_meta = _to_shm(kwargs, shm_registry) if kwargs else None
1493
1463
 
1494
- # Send request
1495
1464
  request = {
1496
1465
  "type": "call_module",
1497
1466
  "module": module,
1498
1467
  "func": func,
1499
- "inputs_path": str(inputs_path) if kwargs else None,
1500
- "outputs_path": str(outputs_path),
1468
+ "kwargs": kwargs_meta,
1501
1469
  }
1502
1470
  response = self._send_request(request, timeout)
1503
1471
 
@@ -1507,17 +1475,13 @@ class PersistentVenvWorker(Worker):
1507
1475
  traceback=response.get("traceback"),
1508
1476
  )
1509
1477
 
1510
- # Load result
1511
- if outputs_path.exists():
1512
- return torch.load(str(outputs_path), weights_only=False)
1478
+ result_meta = response.get("result")
1479
+ if result_meta is not None:
1480
+ return _from_shm(result_meta)
1513
1481
  return None
1514
1482
 
1515
1483
  finally:
1516
- for p in [inputs_path, outputs_path]:
1517
- try:
1518
- p.unlink()
1519
- except:
1520
- pass
1484
+ _cleanup_shm(shm_registry)
1521
1485
 
1522
1486
  def shutdown(self) -> None:
1523
1487
  """Shut down the persistent worker."""
@@ -1569,4 +1533,4 @@ class PersistentVenvWorker(Worker):
1569
1533
 
1570
1534
  def __repr__(self):
1571
1535
  status = "alive" if self.is_alive() else "stopped"
1572
- return f"<PersistentVenvWorker name={self.name!r} status={status}>"
1536
+ return f"<SubprocessWorker name={self.name!r} status={status}>"