From aa3432f2a4796556c61c9d4e23323b247aeda32a Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 24 Feb 2025 20:46:03 -0500
Subject: [PATCH 01/29] Bump various (dev) deps and prefer sys python

Since it turns out there's a few gotchas moving to python 3.13,
- we need to pin to new(er) `trio` which now flips to strict exception
  groups (something to be handled in a follow up patch).
- since we're now using `uv` we should (at least for now) prefer the
  system `python` (over astral's distis) since they compile for
  `libedit` in terms of what the (new) `readline.backend: str` will read
  as; this will break our tab-completion and vi-mode settings in
  the `pdbp` REPL without a user configuring a `~/.editrc`
  appropriately.
- go back to using latest `pdbp` (not a local dev version) since it
  should work fine presuming the previous bullet is addressed.

Lock bumps,
- for now use latest `trio==0.29.0` (which i gotta feeling might have
  broken some existing attempts at strict-eg handling i've tried..)
- update to latest `xonsh`, `pdbp` and its dep `tabcompleter`

Other cleaning,
- put back in various deps "comments" from `poetry` content.
- drop the `xonsh-vox` and `xontrib-vox` dev deps; no `vox` support with
  `uv` rn anyway..
---
 pyproject.toml | 38 +++++++++++++++++++++++++----------
 uv.lock        | 54 +++++++++++++++-----------------------------------
 2 files changed, 43 insertions(+), 49 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index fbacc415..c6eb7532 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,15 +37,13 @@ dependencies = [
   # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#id5
   # TODO, for 3.13 we must go go `0.27` which means we have to
   # disable strict egs or port to handling them internally!
-  # trio='^0.27'
-  "trio>=0.24,<0.25",
+  "trio>0.27",
   "tricycle>=0.4.1,<0.5",
   "wrapt>=1.16.0,<2",
   "colorlog>=6.8.2,<7",
   # built-in multi-actor `pdb` REPL
-  "pdbp>=1.5.0,<2",
+  "pdbp>=1.6,<2", # windows only (from `pdbp`)
   # typed IPC msging
-  # TODO, get back on release once 3.13 support is out!
   "msgspec",
 ]
 
@@ -61,13 +59,9 @@ dev = [
   # `tractor.devx` tooling
   "greenback>=1.2.1,<2",
   "stackscope>=0.2.2,<0.3",
-
-  # xonsh usage/integration (namely as @goodboy's sh of choice Bp)
-  "xonsh>=0.19.1",
-  "xontrib-vox>=0.0.1,<0.0.2",
-  "prompt-toolkit>=3.0.43,<4",
-  "xonsh-vox-tabcomplete>=0.5,<0.6",
   "pyperclip>=1.9.0",
+  "prompt-toolkit>=3.0.50",
+  "xonsh>=0.19.2",
 ]
 # TODO, add these with sane versions; were originally in
 # `requirements-docs.txt`..
@@ -78,21 +72,41 @@ dev = [
 
 # ------ dependency-groups ------
 
+# ------ dependency-groups ------
+
 [tool.uv.sources]
 msgspec = { git = "https://github.com/jcrist/msgspec.git" }
 
+# XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`
+# for the `pp` alias..
+# pdbp = { path = "../pdbp", editable = true }
+
 # ------ tool.uv.sources ------
 # TODO, distributed (multi-host) extensions
 # linux kernel networking
 # 'pyroute2
 
+# ------ tool.uv.sources ------
+
+[tool.uv]
+# XXX NOTE, prefer the sys python bc apparently the distis from
+# `astral` are built in a way that breaks `pdbp`+`tabcompleter`'s
+# likely due to linking against `libedit` over `readline`..
+# |_https://docs.astral.sh/uv/concepts/python-versions/#managed-python-distributions
+# |_https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html#use-of-libedit-on-linux
+#
+# https://docs.astral.sh/uv/reference/settings/#python-preference
+python-preference = 'system'
+
+# ------ tool.uv ------
+
 [tool.hatch.build.targets.sdist]
 include = ["tractor"]
 
 [tool.hatch.build.targets.wheel]
 include = ["tractor"]
 
-# ------ dependency-groups ------
+# ------ tool.hatch ------
 
 [tool.towncrier]
 package = "tractor"
@@ -142,3 +156,5 @@ log_cli = false
 # TODO: maybe some of these layout choices?
 # https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
 # pythonpath = "src"
+
+# ------ tool.pytest ------
diff --git a/uv.lock b/uv.lock
index d89cdfe2..99b9ee23 100644
--- a/uv.lock
+++ b/uv.lock
@@ -300,6 +300,7 @@ dependencies = [
     { name = "colorlog" },
     { name = "msgspec" },
     { name = "pdbp" },
+    { name = "tabcompleter" },
     { name = "tricycle" },
     { name = "trio" },
     { name = "wrapt" },
@@ -314,17 +315,16 @@ dev = [
     { name = "pytest" },
     { name = "stackscope" },
     { name = "xonsh" },
-    { name = "xonsh-vox-tabcomplete" },
-    { name = "xontrib-vox" },
 ]
 
 [package.metadata]
 requires-dist = [
     { name = "colorlog", specifier = ">=6.8.2,<7" },
     { name = "msgspec", git = "https://github.com/jcrist/msgspec.git" },
-    { name = "pdbp", specifier = ">=1.5.0,<2" },
+    { name = "pdbp", specifier = ">=1.6,<2" },
+    { name = "tabcompleter", specifier = ">=1.4.0" },
     { name = "tricycle", specifier = ">=0.4.1,<0.5" },
-    { name = "trio", specifier = ">=0.24,<0.25" },
+    { name = "trio", specifier = ">0.27" },
     { name = "wrapt", specifier = ">=1.16.0,<2" },
 ]
 
@@ -332,13 +332,11 @@ requires-dist = [
 dev = [
     { name = "greenback", specifier = ">=1.2.1,<2" },
     { name = "pexpect", specifier = ">=4.9.0,<5" },
-    { name = "prompt-toolkit", specifier = ">=3.0.43,<4" },
+    { name = "prompt-toolkit", specifier = ">=3.0.50" },
     { name = "pyperclip", specifier = ">=1.9.0" },
     { name = "pytest", specifier = ">=8.2.0,<9" },
     { name = "stackscope", specifier = ">=0.2.2,<0.3" },
-    { name = "xonsh", specifier = ">=0.19.1" },
-    { name = "xonsh-vox-tabcomplete", specifier = ">=0.5,<0.6" },
-    { name = "xontrib-vox", specifier = ">=0.0.1,<0.0.2" },
+    { name = "xonsh", specifier = ">=0.19.2" },
 ]
 
 [[package]]
@@ -355,7 +353,7 @@ wheels = [
 
 [[package]]
 name = "trio"
-version = "0.24.0"
+version = "0.29.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "attrs" },
@@ -365,9 +363,9 @@ dependencies = [
     { name = "sniffio" },
     { name = "sortedcontainers" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/f3/07c152213222c615fe2391b8e1fea0f5af83599219050a549c20fcbd9ba2/trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d", size = 545131 }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/14/fb/9299cf74953f473a15accfdbe2c15218e766bae8c796f2567c83bae03e98/trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c", size = 460205 },
+    { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
 ]
 
 [[package]]
@@ -434,33 +432,13 @@ wheels = [
 
 [[package]]
 name = "xonsh"
-version = "0.19.1"
+version = "0.19.2"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/6e/b54a0b2685535995ee50f655103c463f9d339455c9b08c4bce3e03e7bb17/xonsh-0.19.1.tar.gz", hash = "sha256:5d3de649c909f6d14bc69232219bcbdb8152c830e91ddf17ad169c672397fb97", size = 796468 }
+sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/8c/e6/db44068c5725af9678e37980ae9503165393d51b80dc8517fa4ec74af1cf/xonsh-0.19.1-py310-none-any.whl", hash = "sha256:83eb6610ed3535f8542abd80af9554fb7e2805b0b3f96e445f98d4b5cf1f7046", size = 640686 },
-    { url = "https://files.pythonhosted.org/packages/77/4e/e487e82349866b245c559433c9ba626026a2e66bd17d7f9ac1045082f146/xonsh-0.19.1-py311-none-any.whl", hash = "sha256:c176e515b0260ab803963d1f0924f1e32f1064aa6fd5d791aa0cf6cda3a924ae", size = 640680 },
-    { url = "https://files.pythonhosted.org/packages/5d/88/09060815548219b8f6953a06c247cb5c92d03cbdf7a02a980bda1b5754db/xonsh-0.19.1-py312-none-any.whl", hash = "sha256:fe1266c86b117aced3bdc4d5972420bda715864435d0bd3722d63451e8001036", size = 640604 },
-    { url = "https://files.pythonhosted.org/packages/83/ff/7873cb8184cffeafddbf861712831c2baa2e9dbecdbfd33b1228f0db0019/xonsh-0.19.1-py313-none-any.whl", hash = "sha256:3f158b6fc0bba954e0b989004d4261bafc4bd94c68c2abd75b825da23e5a869c", size = 641166 },
-    { url = "https://files.pythonhosted.org/packages/cc/03/b9f8dd338df0a330011d104e63d4d0acd8bbbc1e990ff049487b6bdf585d/xonsh-0.19.1-py39-none-any.whl", hash = "sha256:a900a6eb87d881a7ef90b1ac8522ba3699582f0bcb1e9abd863d32f6d63faf04", size = 632912 },
-]
-
-[[package]]
-name = "xonsh-vox-tabcomplete"
-version = "0.5"
-source = { registry = "https://pypi.org/simple" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/ab/fd/af0c2ee6c067c2a4dc64ec03598c94de1f6ec5984b3116af917f3add4a16/xonsh_vox_tabcomplete-0.5-py3-none-any.whl", hash = "sha256:9701b198180f167071234e77eab87b7befa97c1873b088d0b3fbbe6d6d8dcaad", size = 14381 },
-]
-
-[[package]]
-name = "xontrib-vox"
-version = "0.0.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "xonsh" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6c/ac/a5db68a1f2e4036f7ff4c8546b1cbe29edee2ff40e0ff931836745988b79/xontrib-vox-0.0.1.tar.gz", hash = "sha256:c1f0b155992b4b0ebe6dcfd651084a8707ade7372f7e456c484d2a85339d9907", size = 16504 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/23/58/dcdf11849c8340033da00669527ce75d8292a4e8d82605c082ed236a081a/xontrib_vox-0.0.1-py3-none-any.whl", hash = "sha256:df2bbb815832db5b04d46684f540eac967ee40ef265add2662a95d6947d04c70", size = 13467 },
+    { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301 },
+    { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 },
+    { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386 },
+    { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 },
+    { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 },
 ]

From 8f369b513203ba67afe8d4ffc4f6155ac2985720 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Tue, 25 Feb 2025 11:20:57 -0500
Subject: [PATCH 02/29] Clean up some imports in `._clustering`

---
 tractor/_clustering.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/tractor/_clustering.py b/tractor/_clustering.py
index 93562fe8..46224d6f 100644
--- a/tractor/_clustering.py
+++ b/tractor/_clustering.py
@@ -19,10 +19,13 @@ Actor cluster helpers.
 
 '''
 from __future__ import annotations
-
-from contextlib import asynccontextmanager as acm
+from contextlib import (
+    asynccontextmanager as acm,
+)
 from multiprocessing import cpu_count
-from typing import AsyncGenerator, Optional
+from typing import (
+    AsyncGenerator,
+)
 
 import trio
 import tractor

From f0deda1fdad4bc3b7055680b5fc0c07ec33b5de5 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Tue, 25 Feb 2025 19:37:30 -0500
Subject: [PATCH 03/29] Flip to `strict_exception_groups=False` in core tns

Since it'll likely need a bit of detailing to get the test suite running
identically with strict egs (exception groups), i've opted to just flip
the switch on a few core nursery scopes for now until as such a time
i can focus enough to port the matching internals.. Xp
---
 tractor/_context.py   |  5 ++++-
 tractor/_root.py      | 17 +++++++++++++----
 tractor/_rpc.py       |  8 ++++++--
 tractor/_supervise.py | 10 ++++++++--
 4 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/tractor/_context.py b/tractor/_context.py
index d4cad88e..1c904b55 100644
--- a/tractor/_context.py
+++ b/tractor/_context.py
@@ -1982,7 +1982,10 @@ async def open_context_from_portal(
     ctxc_from_callee: ContextCancelled|None = None
     try:
         async with (
-            trio.open_nursery() as tn,
+            trio.open_nursery(
+                strict_exception_groups=False,
+            ) as tn,
+
             msgops.maybe_limit_plds(
                 ctx=ctx,
                 spec=ctx_meta.get('pld_spec'),
diff --git a/tractor/_root.py b/tractor/_root.py
index e10b02ef..ed71f69e 100644
--- a/tractor/_root.py
+++ b/tractor/_root.py
@@ -362,7 +362,10 @@ async def open_root_actor(
         )
 
         # start the actor runtime in a new task
-        async with trio.open_nursery() as nursery:
+        async with trio.open_nursery(
+            strict_exception_groups=False,
+            # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
+        ) as nursery:
 
             # ``_runtime.async_main()`` creates an internal nursery
             # and blocks here until any underlying actor(-process)
@@ -457,12 +460,19 @@ def run_daemon(
 
     start_method: str | None = None,
     debug_mode: bool = False,
+
+    # TODO, support `infected_aio=True` mode by,
+    # - calling the appropriate entrypoint-func from `.to_asyncio`
+    # - maybe init-ing `greenback` as done above in
+    #   `open_root_actor()`.
+
     **kwargs
 
 ) -> None:
     '''
-    Spawn daemon actor which will respond to RPC; the main task simply
-    starts the runtime and then sleeps forever.
+    Spawn a root (daemon) actor which will respond to RPC; the main
+    task simply starts the runtime and then blocks via embedded
+    `trio.sleep_forever()`.
 
     This is a very minimal convenience wrapper around starting
     a "run-until-cancelled" root actor which can be started with a set
@@ -475,7 +485,6 @@ def run_daemon(
         importlib.import_module(path)
 
     async def _main():
-
         async with open_root_actor(
             registry_addrs=registry_addrs,
             name=name,
diff --git a/tractor/_rpc.py b/tractor/_rpc.py
index a77c2af7..e170024c 100644
--- a/tractor/_rpc.py
+++ b/tractor/_rpc.py
@@ -620,7 +620,11 @@ async def _invoke(
             tn: trio.Nursery
             rpc_ctx_cs: CancelScope
             async with (
-                trio.open_nursery() as tn,
+                trio.open_nursery(
+                    strict_exception_groups=False,
+                    # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
+
+                ) as tn,
                 msgops.maybe_limit_plds(
                     ctx=ctx,
                     spec=ctx_meta.get('pld_spec'),
@@ -733,8 +737,8 @@ async def _invoke(
         # XXX: do we ever trigger this block any more?
         except (
             BaseExceptionGroup,
-            trio.Cancelled,
             BaseException,
+            trio.Cancelled,
 
         ) as scope_error:
             if (
diff --git a/tractor/_supervise.py b/tractor/_supervise.py
index de268078..b07498b0 100644
--- a/tractor/_supervise.py
+++ b/tractor/_supervise.py
@@ -395,7 +395,10 @@ async def _open_and_supervise_one_cancels_all_nursery(
     # `ActorNursery.start_actor()`).
 
     # errors from this daemon actor nursery bubble up to caller
-    async with trio.open_nursery() as da_nursery:
+    async with trio.open_nursery(
+        strict_exception_groups=False,
+        # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
+    ) as da_nursery:
         try:
             # This is the inner level "run in actor" nursery. It is
             # awaited first since actors spawned in this way (using
@@ -405,7 +408,10 @@ async def _open_and_supervise_one_cancels_all_nursery(
             # immediately raised for handling by a supervisor strategy.
             # As such if the strategy propagates any error(s) upwards
             # the above "daemon actor" nursery will be notified.
-            async with trio.open_nursery() as ria_nursery:
+            async with trio.open_nursery(
+                strict_exception_groups=False,
+                # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
+            ) as ria_nursery:
 
                 an = ActorNursery(
                     actor,

From 41865417244f532cf4c8e518850d840661c0bbd2 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 13:04:37 -0500
Subject: [PATCH 04/29] Expose `hide_tb: bool` from `.open_nursery()`

Such that it gets passed through to `.open_root_actor()` in the
`implicit_runtime==True` case - useful for debugging cases where
`.devx._debug` APIs might be used to avoid REPL clobbering in subactors.
---
 tractor/_root.py      |  8 +++++++-
 tractor/_supervise.py | 21 ++++++++++++++-------
 2 files changed, 21 insertions(+), 8 deletions(-)

diff --git a/tractor/_root.py b/tractor/_root.py
index ed71f69e..2a9beaa3 100644
--- a/tractor/_root.py
+++ b/tractor/_root.py
@@ -111,8 +111,8 @@ async def open_root_actor(
     Runtime init entry point for ``tractor``.
 
     '''
-    __tracebackhide__: bool = hide_tb
     _debug.hide_runtime_frames()
+    __tracebackhide__: bool = hide_tb
 
     # TODO: stick this in a `@cm` defined in `devx._debug`?
     #
@@ -390,6 +390,12 @@ async def open_root_actor(
                 BaseExceptionGroup,
             ) as err:
 
+                # TODO, in beginning to handle the subsubactor with
+                # crashed grandparent cases..
+                #
+                # was_locked: bool = await _debug.maybe_wait_for_debugger(
+                #     child_in_debug=True,
+                # )
                 # XXX NOTE XXX see equiv note inside
                 # `._runtime.Actor._stream_handler()` where in the
                 # non-root or root-that-opened-this-mahually case we
diff --git a/tractor/_supervise.py b/tractor/_supervise.py
index b07498b0..4ecc1a29 100644
--- a/tractor/_supervise.py
+++ b/tractor/_supervise.py
@@ -402,7 +402,7 @@ async def _open_and_supervise_one_cancels_all_nursery(
         try:
             # This is the inner level "run in actor" nursery. It is
             # awaited first since actors spawned in this way (using
-            # ``ActorNusery.run_in_actor()``) are expected to only
+            # `ActorNusery.run_in_actor()`) are expected to only
             # return a single result and then complete (i.e. be canclled
             # gracefully). Errors collected from these actors are
             # immediately raised for handling by a supervisor strategy.
@@ -478,8 +478,8 @@ async def _open_and_supervise_one_cancels_all_nursery(
                             ContextCancelled,
                         }:
                             log.cancel(
-                                'Actor-nursery caught remote cancellation\n\n'
-
+                                'Actor-nursery caught remote cancellation\n'
+                                '\n'
                                 f'{inner_err.tb_str}'
                             )
                         else:
@@ -571,7 +571,9 @@ async def _open_and_supervise_one_cancels_all_nursery(
 @acm
 # @api_frame
 async def open_nursery(
+    hide_tb: bool = False,
     **kwargs,
+    # ^TODO, paramspec for `open_root_actor()`
 
 ) -> typing.AsyncGenerator[ActorNursery, None]:
     '''
@@ -589,7 +591,7 @@ async def open_nursery(
     which cancellation scopes correspond to each spawned subactor set.
 
     '''
-    __tracebackhide__: bool = True
+    __tracebackhide__: bool = hide_tb
     implicit_runtime: bool = False
     actor: Actor = current_actor(err_on_no_runtime=False)
     an: ActorNursery|None = None
@@ -605,7 +607,10 @@ async def open_nursery(
             # mark us for teardown on exit
             implicit_runtime: bool = True
 
-            async with open_root_actor(**kwargs) as actor:
+            async with open_root_actor(
+                hide_tb=hide_tb,
+                **kwargs,
+            ) as actor:
                 assert actor is current_actor()
 
                 try:
@@ -643,8 +648,10 @@ async def open_nursery(
         # show frame on any internal runtime-scope error
         if (
             an
-            and not an.cancelled
-            and an._scope_error
+            and
+            not an.cancelled
+            and
+            an._scope_error
         ):
             __tracebackhide__: bool = False
 

From aa80b555679d4c3016f621c102627ebd270d6488 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 13:16:15 -0500
Subject: [PATCH 05/29] Log format tweaks for sclang reprs

A space here, a newline there..
---
 tractor/_context.py | 5 +++--
 tractor/_entry.py   | 2 +-
 tractor/_rpc.py     | 4 ++--
 tractor/_runtime.py | 7 +++++--
 tractor/_spawn.py   | 7 ++++---
 5 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/tractor/_context.py b/tractor/_context.py
index 1c904b55..eb66aade 100644
--- a/tractor/_context.py
+++ b/tractor/_context.py
@@ -950,7 +950,7 @@ class Context:
             # f'Context.cancel() => {self.chan.uid}\n'
             f'c)=> {self.chan.uid}\n'
             # f'{self.chan.uid}\n'
-            f' |_ @{self.dst_maddr}\n'
+            f'  |_ @{self.dst_maddr}\n'
             f'    >> {self.repr_rpc}\n'
             # f'    >> {self._nsf}() -> {codec}[dict]:\n\n'
             # TODO: pull msg-type from spec re #320
@@ -1003,7 +1003,8 @@ class Context:
                     )
                 else:
                     log.cancel(
-                        'Timed out on cancel request of remote task?\n'
+                        f'Timed out on cancel request of remote task?\n'
+                        f'\n'
                         f'{reminfo}'
                     )
 
diff --git a/tractor/_entry.py b/tractor/_entry.py
index 19dcb9f6..8156d25f 100644
--- a/tractor/_entry.py
+++ b/tractor/_entry.py
@@ -238,7 +238,7 @@ def _trio_main(
             nest_from_op(
                 input_op='>(',  # see syntax ideas above
                 tree_str=actor_info,
-                back_from_op=1,
+                back_from_op=2,  # since "complete"
             )
         )
     logmeth = log.info
diff --git a/tractor/_rpc.py b/tractor/_rpc.py
index e170024c..9e50c5de 100644
--- a/tractor/_rpc.py
+++ b/tractor/_rpc.py
@@ -851,8 +851,8 @@ async def try_ship_error_to_remote(
             log.critical(
                 'IPC transport failure -> '
                 f'failed to ship error to {remote_descr}!\n\n'
-                f'X=> {channel.uid}\n\n'
-
+                f'{type(msg)!r}[{msg.boxed_type}] X=> {channel.uid}\n'
+                f'\n'
                 # TODO: use `.msg.preetty_struct` for this!
                 f'{msg}\n'
             )
diff --git a/tractor/_runtime.py b/tractor/_runtime.py
index 7a00d613..fef92e66 100644
--- a/tractor/_runtime.py
+++ b/tractor/_runtime.py
@@ -1283,7 +1283,8 @@ class Actor:
         msg: str = (
             f'Actor-runtime cancel request from {requester_type}\n\n'
             f'<=c) {requesting_uid}\n'
-            f' |_{self}\n'
+            f'  |_{self}\n'
+            f'\n'
         )
 
         # TODO: what happens here when we self-cancel tho?
@@ -1303,13 +1304,15 @@ class Actor:
                 lock_req_ctx.has_outcome
             ):
                 msg += (
-                    '-> Cancelling active debugger request..\n'
+                    f'\n'
+                    f'-> Cancelling active debugger request..\n'
                     f'|_{_debug.Lock.repr()}\n\n'
                     f'|_{lock_req_ctx}\n\n'
                 )
                 # lock_req_ctx._scope.cancel()
                 # TODO: wrap this in a method-API..
                 debug_req.req_cs.cancel()
+                # if lock_req_ctx:
 
             # self-cancel **all** ongoing RPC tasks
             await self.cancel_rpc_tasks(
diff --git a/tractor/_spawn.py b/tractor/_spawn.py
index 562c7e5b..3159508d 100644
--- a/tractor/_spawn.py
+++ b/tractor/_spawn.py
@@ -327,9 +327,10 @@ async def soft_kill(
     uid: tuple[str, str] = portal.channel.uid
     try:
         log.cancel(
-            'Soft killing sub-actor via portal request\n'
-            f'c)> {portal.chan.uid}\n'
-            f' |_{proc}\n'
+            f'Soft killing sub-actor via portal request\n'
+            f'\n'
+            f'(c=> {portal.chan.uid}\n'
+            f'  |_{proc}\n'
         )
         # wait on sub-proc to signal termination
         await wait_func(proc)

From 6b2809b82e8c5ea47b9afca528acfbfb1868a502 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 13:49:14 -0500
Subject: [PATCH 06/29] Disable tb colors in `._testing.mk_cmd()`

Unset the appropriate cpython osenv var such that our `pexpect` script
runs in the test suite can maintain original matching logic.
---
 tractor/_testing/__init__.py | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/tractor/_testing/__init__.py b/tractor/_testing/__init__.py
index 1f6624e9..43507c33 100644
--- a/tractor/_testing/__init__.py
+++ b/tractor/_testing/__init__.py
@@ -19,7 +19,10 @@ Various helpers/utils for auditing your `tractor` app and/or the
 core runtime.
 
 '''
-from contextlib import asynccontextmanager as acm
+from contextlib import (
+    asynccontextmanager as acm,
+)
+import os
 import pathlib
 
 import tractor
@@ -59,7 +62,12 @@ def mk_cmd(
     exs_subpath: str = 'debugging',
 ) -> str:
     '''
-    Generate a shell command suitable to pass to ``pexpect.spawn()``.
+    Generate a shell command suitable to pass to `pexpect.spawn()`
+    which runs the script as a python program's entrypoint.
+
+    In particular ensure we disable the new tb coloring via unsetting
+    `$PYTHON_COLORS` so that `pexpect` can pattern match without
+    color-escape-codes.
 
     '''
     script_path: pathlib.Path = (
@@ -67,10 +75,15 @@ def mk_cmd(
         / exs_subpath
         / f'{ex_name}.py'
     )
-    return ' '.join([
+    py_cmd: str = ' '.join([
         'python',
         str(script_path)
     ])
+    # XXX, required for py 3.13+
+    # https://docs.python.org/3/using/cmdline.html#using-on-controlling-color
+    # https://docs.python.org/3/using/cmdline.html#envvar-PYTHON_COLORS
+    os.environ['PYTHON_COLORS'] = '0'
+    return py_cmd
 
 
 @acm

From eb18168a4e2cb6d6eed26baea6fdb0e864dbf051 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 18:06:06 -0500
Subject: [PATCH 07/29] A couple more loose-egs flag flips

Namely inside,
- `ActorNursery.open_portal()` which uses
  `.trionics.maybe_open_nursery()` and is now adjusted to
  pass-through `**kwargs` for at least this flag.
- inside the `.trionics.gather_contexts()`.
---
 tractor/_portal.py             |  4 ++++
 tractor/trionics/_broadcast.py |  2 +-
 tractor/trionics/_mngrs.py     | 13 ++++++++++---
 3 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/tractor/_portal.py b/tractor/_portal.py
index f5a66836..7fbf69b2 100644
--- a/tractor/_portal.py
+++ b/tractor/_portal.py
@@ -533,6 +533,10 @@ async def open_portal(
     async with maybe_open_nursery(
         tn,
         shield=shield,
+        strict_exception_groups=False,
+        # ^XXX^ TODO? soo roll our own then ??
+        # -> since we kinda want the "if only one `.exception` then
+        # just raise that" interface?
     ) as tn:
 
         if not channel.connected():
diff --git a/tractor/trionics/_broadcast.py b/tractor/trionics/_broadcast.py
index 154b037d..2286e70d 100644
--- a/tractor/trionics/_broadcast.py
+++ b/tractor/trionics/_broadcast.py
@@ -15,7 +15,7 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 '''
-``tokio`` style broadcast channel.
+`tokio` style broadcast channel.
 https://docs.rs/tokio/1.11.0/tokio/sync/broadcast/index.html
 
 '''
diff --git a/tractor/trionics/_mngrs.py b/tractor/trionics/_mngrs.py
index fd224d65..9a5ed156 100644
--- a/tractor/trionics/_mngrs.py
+++ b/tractor/trionics/_mngrs.py
@@ -57,6 +57,8 @@ async def maybe_open_nursery(
     shield: bool = False,
     lib: ModuleType = trio,
 
+    **kwargs,  # proxy thru
+
 ) -> AsyncGenerator[trio.Nursery, Any]:
     '''
     Create a new nursery if None provided.
@@ -67,7 +69,7 @@ async def maybe_open_nursery(
     if nursery is not None:
         yield nursery
     else:
-        async with lib.open_nursery() as nursery:
+        async with lib.open_nursery(**kwargs) as nursery:
             nursery.cancel_scope.shield = shield
             yield nursery
 
@@ -143,9 +145,14 @@ async def gather_contexts(
             'Use a non-lazy iterator or sequence type intead!'
         )
 
-    async with trio.open_nursery() as n:
+    async with trio.open_nursery(
+        strict_exception_groups=False,
+        # ^XXX^ TODO? soo roll our own then ??
+        # -> since we kinda want the "if only one `.exception` then
+        # just raise that" interface?
+    ) as tn:
         for mngr in mngrs:
-            n.start_soon(
+            tn.start_soon(
                 _enter_and_wait,
                 mngr,
                 unwrapped,

From 8b4ed31d3b4e67009c79c75047f86cd5dab21cd0 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 18:21:19 -0500
Subject: [PATCH 08/29] Handle egs on failed `request_root_stdio_lock()`

Namely when the subactor fails to lock the root, in which case we
try to be very verbose about how/what failed in logging as well
as ensure we cancel the employed IPC ctx.

Implement the outer `BaseException` handler to handle both styles,
- match on an eg (or the prior std cancel excs) only raising a lone
  sub-exc from for former.
- always `as _req_err:` and assign to a new func-global `req_err`
  to enable the above matching.

Other,
- raise `DebugStateError` on `status.subactor_uid != actor_uid`.
- fix a `_repl_fail_report` ref error due to making silly assumptions
  about the `_repl_fail_msg` global; now copy from global as default.
- various log-fmt and logic expression styling tweaks.
- ignore `trio.Cancelled` by default in `open_crash_handler()`.
---
 tractor/devx/_debug.py | 137 +++++++++++++++++++++++++----------------
 1 file changed, 84 insertions(+), 53 deletions(-)

diff --git a/tractor/devx/_debug.py b/tractor/devx/_debug.py
index 04df000f..884c5aea 100644
--- a/tractor/devx/_debug.py
+++ b/tractor/devx/_debug.py
@@ -317,8 +317,6 @@ class Lock:
         we_released: bool = False
         ctx_in_debug: Context|None = cls.ctx_in_debug
         repl_task: Task|Thread|None = DebugStatus.repl_task
-        message: str = ''
-
         try:
             if not DebugStatus.is_main_trio_thread():
                 thread: threading.Thread = threading.current_thread()
@@ -333,6 +331,10 @@ class Lock:
                 return False
 
             task: Task = current_task()
+            message: str = (
+                'TTY NOT RELEASED on behalf of caller\n'
+                f'|_{task}\n'
+            )
 
             # sanity check that if we're the root actor
             # the lock is marked as such.
@@ -347,11 +349,6 @@ class Lock:
             else:
                 assert DebugStatus.repl_task is not task
 
-            message: str = (
-                'TTY lock was NOT released on behalf of caller\n'
-                f'|_{task}\n'
-            )
-
             lock: trio.StrictFIFOLock = cls._debug_lock
             owner: Task = lock.statistics().owner
             if (
@@ -366,23 +363,21 @@ class Lock:
                 # correct task, greenback-spawned-task and/or thread
                 # being set to the `.repl_task` such that the above
                 # condition matches and we actually release the lock.
+                #
                 # This is particular of note from `.pause_from_sync()`!
-
             ):
                 cls._debug_lock.release()
                 we_released: bool = True
                 if repl_task:
                     message: str = (
-                        'Lock released on behalf of root-actor-local REPL owner\n'
+                        'TTY released on behalf of root-actor-local REPL owner\n'
                         f'|_{repl_task}\n'
                     )
                 else:
                     message: str = (
-                        'TTY lock released by us on behalf of remote peer?\n'
-                        f'|_ctx_in_debug: {ctx_in_debug}\n\n'
+                        'TTY released by us on behalf of remote peer?\n'
+                        f'{ctx_in_debug}\n'
                     )
-                    # mk_pdb().set_trace()
-                # elif owner:
 
         except RuntimeError as rte:
             log.exception(
@@ -400,7 +395,8 @@ class Lock:
             req_handler_finished: trio.Event|None = Lock.req_handler_finished
             if (
                 not lock_stats.owner
-                and req_handler_finished is None
+                and
+                req_handler_finished is None
             ):
                 message += (
                     '-> No new task holds the TTY lock!\n\n'
@@ -418,8 +414,8 @@ class Lock:
                     repl_task
                 )
                 message += (
-                    f'A non-caller task still owns this lock on behalf of '
-                    f'`{behalf_of_task}`\n'
+                    f'A non-caller task still owns this lock on behalf of\n'
+                    f'{behalf_of_task}\n'
                     f'lock owner task: {lock_stats.owner}\n'
                 )
 
@@ -447,8 +443,6 @@ class Lock:
 
             if message:
                 log.devx(message)
-            else:
-                import pdbp; pdbp.set_trace()
 
         return we_released
 
@@ -668,10 +662,11 @@ async def lock_stdio_for_peer(
         fail_reason: str = (
             f'on behalf of peer\n\n'
             f'x)<=\n'
-            f'  |_{subactor_task_uid!r}@{ctx.chan.uid!r}\n\n'
-
+            f'   |_{subactor_task_uid!r}@{ctx.chan.uid!r}\n'
+            f'\n'
             'Forcing `Lock.release()` due to acquire failure!\n\n'
-            f'x)=> {ctx}\n'
+            f'x)=>\n'
+            f'   {ctx}'
         )
         if isinstance(req_err, trio.Cancelled):
             fail_reason = (
@@ -1179,7 +1174,7 @@ async def request_root_stdio_lock(
     log.devx(
         'Initing stdio-lock request task with root actor'
     )
-    # TODO: likely we can implement this mutex more generally as
+    # TODO: can we implement this mutex more generally as
     #      a `._sync.Lock`?
     # -[ ] simply add the wrapping needed for the debugger specifics?
     #   - the `__pld_spec__` impl and maybe better APIs for the client
@@ -1190,6 +1185,7 @@ async def request_root_stdio_lock(
     #   - https://docs.python.org/3.8/library/multiprocessing.html#multiprocessing.RLock
     DebugStatus.req_finished = trio.Event()
     DebugStatus.req_task = current_task()
+    req_err: BaseException|None = None
     try:
         from tractor._discovery import get_root
         # NOTE: we need this to ensure that this task exits
@@ -1212,6 +1208,7 @@ async def request_root_stdio_lock(
             # )
             DebugStatus.req_cs = req_cs
             req_ctx: Context|None = None
+            ctx_eg: BaseExceptionGroup|None = None
             try:
                 # TODO: merge into single async with ?
                 async with get_root() as portal:
@@ -1242,7 +1239,12 @@ async def request_root_stdio_lock(
                         )
 
                         # try:
-                        assert status.subactor_uid == actor_uid
+                        if (locker := status.subactor_uid) != actor_uid:
+                            raise DebugStateError(
+                                f'Root actor locked by another peer !?\n'
+                                f'locker: {locker!r}\n'
+                                f'actor_uid: {actor_uid}\n'
+                            )
                         assert status.cid
                         # except AttributeError:
                         #     log.exception('failed pldspec asserts!')
@@ -1279,10 +1281,11 @@ async def request_root_stdio_lock(
                         f'Exitting {req_ctx.side!r}-side of locking req_ctx\n'
                     )
 
-            except (
+            except* (
                 tractor.ContextCancelled,
                 trio.Cancelled,
-            ):
+            ) as _taskc_eg:
+                ctx_eg = _taskc_eg
                 log.cancel(
                     'Debug lock request was CANCELLED?\n\n'
                     f'<=c) {req_ctx}\n'
@@ -1291,21 +1294,23 @@ async def request_root_stdio_lock(
                 )
                 raise
 
-            except (
+            except* (
                 BaseException,
-            ) as ctx_err:
+            ) as _ctx_eg:
+                ctx_eg = _ctx_eg
                 message: str = (
-                    'Failed during debug request dialog with root actor?\n\n'
+                    'Failed during debug request dialog with root actor?\n'
                 )
                 if (req_ctx := DebugStatus.req_ctx):
                     message += (
-                        f'<=x) {req_ctx}\n\n'
+                        f'<=x)\n'
+                        f' |_{req_ctx}\n'
                         f'Cancelling IPC ctx!\n'
                     )
                     try:
                         await req_ctx.cancel()
                     except trio.ClosedResourceError  as terr:
-                        ctx_err.add_note(
+                        ctx_eg.add_note(
                             # f'Failed with {type(terr)!r} x)> `req_ctx.cancel()` '
                             f'Failed with `req_ctx.cancel()` <x) {type(terr)!r} '
                         )
@@ -1314,21 +1319,45 @@ async def request_root_stdio_lock(
                     message += 'Failed in `Portal.open_context()` call ??\n'
 
                 log.exception(message)
-                ctx_err.add_note(message)
-                raise ctx_err
+                ctx_eg.add_note(message)
+                raise ctx_eg
 
-    except (
-        tractor.ContextCancelled,
-        trio.Cancelled,
-    ):
-        log.cancel(
-            'Debug lock request CANCELLED?\n'
-            f'{req_ctx}\n'
-        )
-        raise
+    except BaseException as _req_err:
+        req_err = _req_err
+
+        # XXX NOTE, since new `trio` enforces strict egs by default
+        # we have to always handle the eg explicitly given the
+        # `Portal.open_context()` call above (which implicitly opens
+        # a nursery).
+        match req_err:
+            case BaseExceptionGroup():
+                # for an eg of just one taskc, just unpack and raise
+                # since we want to propagate a plane ol' `Cancelled`
+                # up from the `.pause()` call.
+                excs: list[BaseException] = req_err.exceptions
+                if (
+                    len(excs) == 1
+                    and
+                    type(exc := excs[0]) in (
+                        tractor.ContextCancelled,
+                        trio.Cancelled,
+                    )
+                ):
+                    log.cancel(
+                        'Debug lock request CANCELLED?\n'
+                        f'{req_ctx}\n'
+                    )
+                    raise exc
+            case (
+                tractor.ContextCancelled(),
+                trio.Cancelled(),
+            ):
+                log.cancel(
+                    'Debug lock request CANCELLED?\n'
+                    f'{req_ctx}\n'
+                )
+                raise exc
 
-    except BaseException as req_err:
-        # log.error('Failed to request root stdio-lock?')
         DebugStatus.req_err = req_err
         DebugStatus.release()
 
@@ -1343,7 +1372,7 @@ async def request_root_stdio_lock(
             'Failed during stdio-locking dialog from root actor\n\n'
 
             f'<=x)\n'
-            f'|_{DebugStatus.req_ctx}\n'
+            f'  |_{DebugStatus.req_ctx}\n'
         ) from req_err
 
     finally:
@@ -1406,7 +1435,7 @@ def any_connected_locker_child() -> bool:
     actor: Actor = current_actor()
 
     if not is_root_process():
-        raise RuntimeError('This is a root-actor only API!')
+        raise InternalError('This is a root-actor only API!')
 
     if (
         (ctx := Lock.ctx_in_debug)
@@ -2143,11 +2172,12 @@ async def _pause(
     # `_enter_repl_sync()` into a common @cm?
     except BaseException as _pause_err:
         pause_err: BaseException = _pause_err
+        _repl_fail_report: str|None = _repl_fail_msg
         if isinstance(pause_err, bdb.BdbQuit):
             log.devx(
                 'REPL for pdb was explicitly quit!\n'
             )
-            _repl_fail_msg = None
+            _repl_fail_report = None
 
         # when the actor is mid-runtime cancellation the
         # `Actor._service_n` might get closed before we can spawn
@@ -2167,16 +2197,16 @@ async def _pause(
             return
 
         elif isinstance(pause_err, trio.Cancelled):
-            _repl_fail_msg = (
+            _repl_fail_report += (
                 'You called `tractor.pause()` from an already cancelled scope!\n\n'
                 'Consider `await tractor.pause(shield=True)` to make it work B)\n'
             )
 
         else:
-            _repl_fail_msg += f'on behalf of {repl_task} ??\n'
+            _repl_fail_report += f'on behalf of {repl_task} ??\n'
 
-        if _repl_fail_msg:
-            log.exception(_repl_fail_msg)
+        if _repl_fail_report:
+            log.exception(_repl_fail_report)
 
         if not actor.is_infected_aio():
             DebugStatus.release(cancel_req_task=True)
@@ -3051,7 +3081,8 @@ async def maybe_wait_for_debugger(
 
     if (
         not debug_mode()
-        and not child_in_debug
+        and
+        not child_in_debug
     ):
         return False
 
@@ -3109,7 +3140,7 @@ async def maybe_wait_for_debugger(
                 logmeth(
                     msg
                     +
-                    '\nRoot is waiting on tty lock to release from\n\n'
+                    '\n^^ Root is waiting on tty lock release.. ^^\n'
                     # f'{caller_frame_info}\n'
                 )
 
@@ -3172,11 +3203,11 @@ async def maybe_wait_for_debugger(
 @cm
 def open_crash_handler(
     catch: set[BaseException] = {
-        # Exception,
         BaseException,
     },
     ignore: set[BaseException] = {
         KeyboardInterrupt,
+        trio.Cancelled,
     },
     tb_hide: bool = True,
 ):

From 8f774f52b15f79fded6fe552d4ef2af845acd4e0 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 26 Feb 2025 18:37:43 -0500
Subject: [PATCH 09/29] Another loose-egs flag in
 `test_child_manages_service_nursery`

---
 tests/test_child_manages_service_nursery.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tests/test_child_manages_service_nursery.py b/tests/test_child_manages_service_nursery.py
index 21fb3920..956fccd2 100644
--- a/tests/test_child_manages_service_nursery.py
+++ b/tests/test_child_manages_service_nursery.py
@@ -117,7 +117,9 @@ async def open_actor_local_nursery(
     ctx: tractor.Context,
 ):
     global _nursery
-    async with trio.open_nursery() as n:
+    async with trio.open_nursery(
+        strict_exception_groups=False,
+    ) as n:
         _nursery = n
         await ctx.started()
         await trio.sleep(10)

From 87342696a1b3e7bf5447b75ef2950a167f93632b Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 11:14:32 -0500
Subject: [PATCH 10/29] Expose `._state.debug_mode()` predicate at top level

---
 tractor/__init__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tractor/__init__.py b/tractor/__init__.py
index a27a3b59..6ddbf199 100644
--- a/tractor/__init__.py
+++ b/tractor/__init__.py
@@ -44,6 +44,7 @@ from ._state import (
     current_actor as current_actor,
     is_root_process as is_root_process,
     current_ipc_ctx as current_ipc_ctx,
+    debug_mode as debug_mode
 )
 from ._exceptions import (
     ContextCancelled as ContextCancelled,
@@ -66,3 +67,4 @@ from ._root import (
 from ._ipc import Channel as Channel
 from ._portal import Portal as Portal
 from ._runtime import Actor as Actor
+from . import hilevel as hilevel

From 13572151aa6fbff8f50487249c9b100a1588aae8 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:17:51 -0500
Subject: [PATCH 11/29] Moar sclang log fmting tweaks

---
 tractor/_ipc.py       | 12 ++++++------
 tractor/_rpc.py       |  2 +-
 tractor/_state.py     |  1 +
 tractor/_streaming.py |  2 +-
 4 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/tractor/_ipc.py b/tractor/_ipc.py
index a1cb0359..83186147 100644
--- a/tractor/_ipc.py
+++ b/tractor/_ipc.py
@@ -255,8 +255,8 @@ class MsgpackTCPStream(MsgTransport):
                 raise TransportClosed(
                     message=(
                         f'IPC transport already closed by peer\n'
-                        f'x)> {type(trans_err)}\n'
-                        f' |_{self}\n'
+                        f'x]> {type(trans_err)}\n'
+                        f'  |_{self}\n'
                     ),
                     loglevel=loglevel,
                 ) from trans_err
@@ -273,8 +273,8 @@ class MsgpackTCPStream(MsgTransport):
                 raise TransportClosed(
                     message=(
                         f'IPC transport already manually closed locally?\n'
-                        f'x)> {type(closure_err)} \n'
-                        f' |_{self}\n'
+                        f'x]> {type(closure_err)} \n'
+                        f'  |_{self}\n'
                     ),
                     loglevel='error',
                     raise_on_report=(
@@ -289,8 +289,8 @@ class MsgpackTCPStream(MsgTransport):
                 raise TransportClosed(
                     message=(
                         f'IPC transport already gracefully closed\n'
-                        f')>\n'
-                        f'|_{self}\n'
+                        f']>\n'
+                        f' |_{self}\n'
                     ),
                     loglevel='transport',
                     # cause=???  # handy or no?
diff --git a/tractor/_rpc.py b/tractor/_rpc.py
index 9e50c5de..086cfff6 100644
--- a/tractor/_rpc.py
+++ b/tractor/_rpc.py
@@ -851,7 +851,7 @@ async def try_ship_error_to_remote(
             log.critical(
                 'IPC transport failure -> '
                 f'failed to ship error to {remote_descr}!\n\n'
-                f'{type(msg)!r}[{msg.boxed_type}] X=> {channel.uid}\n'
+                f'{type(msg)!r}[{msg.boxed_type_str}] X=> {channel.uid}\n'
                 f'\n'
                 # TODO: use `.msg.preetty_struct` for this!
                 f'{msg}\n'
diff --git a/tractor/_state.py b/tractor/_state.py
index a87ad36b..79c8bdea 100644
--- a/tractor/_state.py
+++ b/tractor/_state.py
@@ -108,6 +108,7 @@ def is_main_process() -> bool:
     return mp.current_process().name == 'MainProcess'
 
 
+# TODO, more verby name?
 def debug_mode() -> bool:
     '''
     Bool determining if "debug mode" is on which enables
diff --git a/tractor/_streaming.py b/tractor/_streaming.py
index bc87164e..58e9b069 100644
--- a/tractor/_streaming.py
+++ b/tractor/_streaming.py
@@ -376,7 +376,7 @@ class MsgStream(trio.abc.Channel):
                 f'Stream self-closed by {self._ctx.side!r}-side before EoC\n'
                 # } bc a stream is a "scope"/msging-phase inside an IPC
                 f'x}}>\n'
-                f'|_{self}\n'
+                f'  |_{self}\n'
             )
             log.cancel(message)
             self._eoc = trio.EndOfChannel(message)

From fdf934d02dc48ee03c4633f51a90da90565f5a72 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:18:10 -0500
Subject: [PATCH 12/29] Hide `open_nursery()` frame by def

---
 tractor/_supervise.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tractor/_supervise.py b/tractor/_supervise.py
index 4ecc1a29..bc6bc983 100644
--- a/tractor/_supervise.py
+++ b/tractor/_supervise.py
@@ -571,7 +571,7 @@ async def _open_and_supervise_one_cancels_all_nursery(
 @acm
 # @api_frame
 async def open_nursery(
-    hide_tb: bool = False,
+    hide_tb: bool = True,
     **kwargs,
     # ^TODO, paramspec for `open_root_actor()`
 

From 6e68f516176115fb0b56725e4b25f3b04749bba4 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:19:11 -0500
Subject: [PATCH 13/29] Fix `roundtripped` ref error in
 `validate_payload_msg()`

---
 tractor/msg/_ops.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/tractor/msg/_ops.py b/tractor/msg/_ops.py
index 2faadb9f..615ad0c8 100644
--- a/tractor/msg/_ops.py
+++ b/tractor/msg/_ops.py
@@ -796,6 +796,7 @@ def validate_payload_msg(
     __tracebackhide__: bool = hide_tb
     codec: MsgCodec = current_codec()
     msg_bytes: bytes = codec.encode(pld_msg)
+    roundtripped: Started|None = None
     try:
         roundtripped: Started = codec.decode(msg_bytes)
         ctx: Context = getattr(ipc, 'ctx', ipc)
@@ -832,9 +833,13 @@ def validate_payload_msg(
                 verb_header='Trying to send ',
                 is_invalid_payload=True,
             )
-        except BaseException:
+        except BaseException as _be:
+            if not roundtripped:
+                raise verr
+
+            be = _be
             __tracebackhide__: bool = False
-            raise
+            raise be
 
         if not raise_mte:
             return mte

From c453623b9baa8e394871356a654a200d12bc85e8 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:20:33 -0500
Subject: [PATCH 14/29] Go to loose egs in `Actor` root & service nurseries
 (for now..)

---
 tractor/_runtime.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/tractor/_runtime.py b/tractor/_runtime.py
index fef92e66..e7faaedf 100644
--- a/tractor/_runtime.py
+++ b/tractor/_runtime.py
@@ -1721,11 +1721,15 @@ async def async_main(
         # parent is kept alive as a resilient service until
         # cancellation steps have (mostly) occurred in
         # a deterministic way.
-        async with trio.open_nursery() as root_nursery:
+        async with trio.open_nursery(
+            strict_exception_groups=False,
+        ) as root_nursery:
             actor._root_n = root_nursery
             assert actor._root_n
 
-            async with trio.open_nursery() as service_nursery:
+            async with trio.open_nursery(
+                strict_exception_groups=False,
+            ) as service_nursery:
                 # This nursery is used to handle all inbound
                 # connections to us such that if the TCP server
                 # is killed, connections can continue to process

From 8f7c022afe4666100111431448e2255399550b52 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:24:29 -0500
Subject: [PATCH 15/29] Various test tweaks related to 3.13 egs

Including changes like,
- loose eg flagging in various test emedded `trio.open_nursery()`s.
- changes to eg handling (like using `except*`).
- added `debug_mode` integration to tests that needed some REPLin
  in order to figure out appropriate updates.
---
 .../ipc_failure_during_stream.py               |  8 +++++---
 examples/debugging/multi_daemon_subactors.py   |  8 +++++---
 .../debugging/multi_subactor_root_errors.py    |  2 +-
 examples/full_fledged_streaming_service.py     |  2 +-
 tests/devx/test_debugger.py                    |  9 ++++++---
 tests/test_advanced_faults.py                  | 12 ++++--------
 tests/test_advanced_streaming.py               | 10 +++++++++-
 tests/test_cancellation.py                     | 10 +++++++++-
 tests/test_caps_based_msging.py                |  8 ++++----
 tests/test_child_manages_service_nursery.py    | 18 +++++++++---------
 tests/test_discovery.py                        |  4 +++-
 tests/test_trioisms.py                         |  5 ++++-
 12 files changed, 60 insertions(+), 36 deletions(-)

diff --git a/examples/advanced_faults/ipc_failure_during_stream.py b/examples/advanced_faults/ipc_failure_during_stream.py
index 60b28c3e..950d5a6f 100644
--- a/examples/advanced_faults/ipc_failure_during_stream.py
+++ b/examples/advanced_faults/ipc_failure_during_stream.py
@@ -62,7 +62,9 @@ async def recv_and_spawn_net_killers(
     await ctx.started()
     async with (
         ctx.open_stream() as stream,
-        trio.open_nursery() as n,
+        trio.open_nursery(
+            strict_exception_groups=False,
+        ) as tn,
     ):
         async for i in stream:
             print(f'child echoing {i}')
@@ -77,11 +79,11 @@ async def recv_and_spawn_net_killers(
                 i >= break_ipc_after
             ):
                 broke_ipc = True
-                n.start_soon(
+                tn.start_soon(
                     iter_ipc_stream,
                     stream,
                 )
-                n.start_soon(
+                tn.start_soon(
                     partial(
                         break_ipc_then_error,
                         stream=stream,
diff --git a/examples/debugging/multi_daemon_subactors.py b/examples/debugging/multi_daemon_subactors.py
index 7844ccef..844a228a 100644
--- a/examples/debugging/multi_daemon_subactors.py
+++ b/examples/debugging/multi_daemon_subactors.py
@@ -21,11 +21,13 @@ async def name_error():
 
 
 async def main():
-    """Test breakpoint in a streaming actor.
-    """
+    '''
+    Test breakpoint in a streaming actor.
+
+    '''
     async with tractor.open_nursery(
         debug_mode=True,
-        # loglevel='cancel',
+        loglevel='cancel',
         # loglevel='devx',
     ) as n:
 
diff --git a/examples/debugging/multi_subactor_root_errors.py b/examples/debugging/multi_subactor_root_errors.py
index 640f2223..31bb7dd1 100644
--- a/examples/debugging/multi_subactor_root_errors.py
+++ b/examples/debugging/multi_subactor_root_errors.py
@@ -40,7 +40,7 @@ async def main():
     """
     async with tractor.open_nursery(
         debug_mode=True,
-        # loglevel='cancel',
+        loglevel='devx',
     ) as n:
 
         # spawn both actors
diff --git a/examples/full_fledged_streaming_service.py b/examples/full_fledged_streaming_service.py
index be4c372e..d859f647 100644
--- a/examples/full_fledged_streaming_service.py
+++ b/examples/full_fledged_streaming_service.py
@@ -91,7 +91,7 @@ async def main() -> list[int]:
     an: ActorNursery
     async with tractor.open_nursery(
         loglevel='cancel',
-        debug_mode=True,
+        # debug_mode=True,
     ) as an:
 
         seed = int(1e3)
diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py
index 8b723c6f..171e983e 100644
--- a/tests/devx/test_debugger.py
+++ b/tests/devx/test_debugger.py
@@ -309,10 +309,13 @@ def test_subactor_breakpoint(
     child.expect(EOF)
 
     assert in_prompt_msg(
-        child,
-        ['RemoteActorError:',
+        child, [
+        'MessagingError:',
+        'RemoteActorError:',
          "('breakpoint_forever'",
-         'bdb.BdbQuit',]
+         'bdb.BdbQuit',
+        ],
+        pause_on_false=True,
     )
 
 
diff --git a/tests/test_advanced_faults.py b/tests/test_advanced_faults.py
index a4d17791..85bac932 100644
--- a/tests/test_advanced_faults.py
+++ b/tests/test_advanced_faults.py
@@ -3,7 +3,6 @@ Sketchy network blackoutz, ugly byzantine gens, puedes eschuchar la
 cancelacion?..
 
 '''
-import itertools
 from functools import partial
 from types import ModuleType
 
@@ -230,13 +229,10 @@ def test_ipc_channel_break_during_stream(
     # get raw instance from pytest wrapper
     value = excinfo.value
     if isinstance(value, ExceptionGroup):
-        value = next(
-            itertools.dropwhile(
-                lambda exc: not isinstance(exc, expect_final_exc),
-                value.exceptions,
-            )
-        )
-        assert value
+        excs = value.exceptions
+        assert len(excs) == 1
+        final_exc = excs[0]
+        assert isinstance(final_exc, expect_final_exc)
 
 
 @tractor.context
diff --git a/tests/test_advanced_streaming.py b/tests/test_advanced_streaming.py
index 3134b9c2..64f24167 100644
--- a/tests/test_advanced_streaming.py
+++ b/tests/test_advanced_streaming.py
@@ -307,7 +307,15 @@ async def inf_streamer(
 
     async with (
         ctx.open_stream() as stream,
-        trio.open_nursery() as tn,
+
+        # XXX TODO, INTERESTING CASE!!
+        # - if we don't collapse the eg then the embedded
+        # `trio.EndOfChannel` doesn't propagate directly to the above
+        # .open_stream() parent, resulting in it also raising instead
+        # of gracefully absorbing as normal.. so how to handle?
+        trio.open_nursery(
+            strict_exception_groups=False,
+        ) as tn,
     ):
         async def close_stream_on_sentinel():
             async for msg in stream:
diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py
index ece4d3c7..ca14ae4b 100644
--- a/tests/test_cancellation.py
+++ b/tests/test_cancellation.py
@@ -519,7 +519,9 @@ def test_cancel_via_SIGINT_other_task(
     async def main():
         # should never timeout since SIGINT should cancel the current program
         with trio.fail_after(timeout):
-            async with trio.open_nursery() as n:
+            async with trio.open_nursery(
+                strict_exception_groups=False,
+            ) as n:
                 await n.start(spawn_and_sleep_forever)
                 if 'mp' in spawn_backend:
                     time.sleep(0.1)
@@ -612,6 +614,12 @@ def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
                     nurse.start_soon(delayed_kbi)
 
                     await p.run(do_nuthin)
+
+        # need to explicitly re-raise the lone kbi..now
+        except* KeyboardInterrupt as kbi_eg:
+            assert (len(excs := kbi_eg.exceptions) == 1)
+            raise excs[0]
+
         finally:
             duration = time.time() - start
             if duration > timeout:
diff --git a/tests/test_caps_based_msging.py b/tests/test_caps_based_msging.py
index 6064c2cf..ba2bb101 100644
--- a/tests/test_caps_based_msging.py
+++ b/tests/test_caps_based_msging.py
@@ -874,13 +874,13 @@ def chk_pld_type(
     return roundtrip
 
 
-def test_limit_msgspec():
-
+def test_limit_msgspec(
+    debug_mode: bool,
+):
     async def main():
         async with tractor.open_root_actor(
-            debug_mode=True
+            debug_mode=debug_mode,
         ):
-
             # ensure we can round-trip a boxing `PayloadMsg`
             assert chk_pld_type(
                 payload_spec=Any,
diff --git a/tests/test_child_manages_service_nursery.py b/tests/test_child_manages_service_nursery.py
index 956fccd2..540e9b2e 100644
--- a/tests/test_child_manages_service_nursery.py
+++ b/tests/test_child_manages_service_nursery.py
@@ -95,8 +95,8 @@ async def trio_main(
 
     # stash a "service nursery" as "actor local" (aka a Python global)
     global _nursery
-    n = _nursery
-    assert n
+    tn = _nursery
+    assert tn
 
     async def consume_stream():
         async with wrapper_mngr() as stream:
@@ -104,10 +104,10 @@ async def trio_main(
                 print(msg)
 
     # run 2 tasks to ensure broadcaster chan use
-    n.start_soon(consume_stream)
-    n.start_soon(consume_stream)
+    tn.start_soon(consume_stream)
+    tn.start_soon(consume_stream)
 
-    n.start_soon(trio_sleep_and_err)
+    tn.start_soon(trio_sleep_and_err)
 
     await trio.sleep_forever()
 
@@ -119,8 +119,8 @@ async def open_actor_local_nursery(
     global _nursery
     async with trio.open_nursery(
         strict_exception_groups=False,
-    ) as n:
-        _nursery = n
+    ) as tn:
+        _nursery = tn
         await ctx.started()
         await trio.sleep(10)
         # await trio.sleep(1)
@@ -134,7 +134,7 @@ async def open_actor_local_nursery(
         # never yields back.. aka a scenario where the
         # ``tractor.context`` task IS NOT in the service n's cancel
         # scope.
-        n.cancel_scope.cancel()
+        tn.cancel_scope.cancel()
 
 
 @pytest.mark.parametrize(
@@ -159,7 +159,7 @@ def test_actor_managed_trio_nursery_task_error_cancels_aio(
         async with tractor.open_nursery() as n:
             p = await n.start_actor(
                 'nursery_mngr',
-                infect_asyncio=asyncio_mode,
+                infect_asyncio=asyncio_mode,  # TODO, is this enabling debug mode?
                 enable_modules=[__name__],
             )
             async with (
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index 508fdbe1..8d014ce3 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -181,7 +181,9 @@ async def spawn_and_check_registry(
 
             try:
                 async with tractor.open_nursery() as n:
-                    async with trio.open_nursery() as trion:
+                    async with trio.open_nursery(
+                        strict_exception_groups=False,
+                    ) as trion:
 
                         portals = {}
                         for i in range(3):
diff --git a/tests/test_trioisms.py b/tests/test_trioisms.py
index fad99f11..449ddcc2 100644
--- a/tests/test_trioisms.py
+++ b/tests/test_trioisms.py
@@ -101,6 +101,7 @@ def test_stashed_child_nursery(use_start_soon):
 def test_acm_embedded_nursery_propagates_enter_err(
     canc_from_finally: bool,
     unmask_from_canc: bool,
+    debug_mode: bool,
 ):
     '''
     Demo how a masking `trio.Cancelled` could be handled by unmasking from the
@@ -174,7 +175,9 @@ def test_acm_embedded_nursery_propagates_enter_err(
                     await trio.lowlevel.checkpoint()
 
     async def _main():
-        with tractor.devx.open_crash_handler() as bxerr:
+        with tractor.devx.maybe_open_crash_handler(
+            pdb=debug_mode,
+        ) as bxerr:
             assert not bxerr.value
 
             async with (

From e5bcefb5757aee530ab8cecf6bc011df61f0b1f0 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 12:32:25 -0500
Subject: [PATCH 16/29] Add (masked) meta-debug-fixture for determining if
 `debug_mode` is set in harness..

---
 tests/conftest.py | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/tests/conftest.py b/tests/conftest.py
index 810b642a..674767ff 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -75,7 +75,10 @@ def pytest_configure(config):
 
 @pytest.fixture(scope='session')
 def debug_mode(request):
-    return request.config.option.tractor_debug_mode
+    debug_mode: bool = request.config.option.tractor_debug_mode
+    # if debug_mode:
+    #     breakpoint()
+    return debug_mode
 
 
 @pytest.fixture(scope='session', autouse=True)
@@ -92,6 +95,12 @@ def spawn_backend(request) -> str:
     return request.config.option.spawn_backend
 
 
+# @pytest.fixture(scope='function', autouse=True)
+# def debug_enabled(request) -> str:
+#     from tractor import _state
+#     if _state._runtime_vars['_debug_mode']:
+#         breakpoint()
+
 _ci_env: bool = os.environ.get('CI', False)
 
 

From 22f405a707d562fa61a014af64e6eb26a91ef4ed Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 13:57:54 -0500
Subject: [PATCH 17/29] Another couple loose-ifies for discovery and advanced
 fault suites

---
 tests/test_advanced_faults.py | 11 ++++++-----
 tests/test_discovery.py       |  4 +++-
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/tests/test_advanced_faults.py b/tests/test_advanced_faults.py
index 85bac932..de8a0e1c 100644
--- a/tests/test_advanced_faults.py
+++ b/tests/test_advanced_faults.py
@@ -255,15 +255,16 @@ async def break_ipc_after_started(
 
 def test_stream_closed_right_after_ipc_break_and_zombie_lord_engages():
     '''
-    Verify that is a subactor's IPC goes down just after bringing up a stream
-    the parent can trigger a SIGINT and the child will be reaped out-of-IPC by
-    the localhost process supervision machinery: aka "zombie lord".
+    Verify that is a subactor's IPC goes down just after bringing up
+    a stream the parent can trigger a SIGINT and the child will be
+    reaped out-of-IPC by the localhost process supervision machinery:
+    aka "zombie lord".
 
     '''
     async def main():
         with trio.fail_after(3):
-            async with tractor.open_nursery() as n:
-                portal = await n.start_actor(
+            async with tractor.open_nursery() as an:
+                portal = await an.start_actor(
                     'ipc_breaker',
                     enable_modules=[__name__],
                 )
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index 8d014ce3..87455983 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -318,7 +318,9 @@ async def close_chans_before_nursery(
                         async with portal2.open_stream_from(
                             stream_forever
                         ) as agen2:
-                            async with trio.open_nursery() as n:
+                            async with trio.open_nursery(
+                                strict_exception_groups=False,
+                            ) as n:
                                 n.start_soon(streamer, agen1)
                                 n.start_soon(cancel, use_signal, .5)
                                 try:

From 3ad558230ad2d723d507ab20d8f32ac5679a8c45 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 17:55:07 -0500
Subject: [PATCH 18/29] Fix docs tests with yet another loosie-goosie

So the KBI propagates up to the actor nursery scope and also avoid
running any `examples/multihost/` subdir scripts.
---
 examples/quick_cluster.py   | 19 +++++++++++--------
 tests/test_docs_examples.py | 37 +++++++++++++++++++++++--------------
 2 files changed, 34 insertions(+), 22 deletions(-)

diff --git a/examples/quick_cluster.py b/examples/quick_cluster.py
index ca692a90..2378a3cf 100644
--- a/examples/quick_cluster.py
+++ b/examples/quick_cluster.py
@@ -3,20 +3,18 @@ import trio
 import tractor
 
 
-async def sleepy_jane():
-    uid = tractor.current_actor().uid
+async def sleepy_jane() -> None:
+    uid: tuple = tractor.current_actor().uid
     print(f'Yo i am actor {uid}')
     await trio.sleep_forever()
 
 
 async def main():
     '''
-    Spawn a flat actor cluster, with one process per
-    detected core.
+    Spawn a flat actor cluster, with one process per detected core.
 
     '''
     portal_map: dict[str, tractor.Portal]
-    results: dict[str, str]
 
     # look at this hip new syntax!
     async with (
@@ -25,11 +23,16 @@ async def main():
             modules=[__name__]
         ) as portal_map,
 
-        trio.open_nursery() as n,
+        trio.open_nursery(
+            strict_exception_groups=False,
+        ) as tn,
     ):
 
         for (name, portal) in portal_map.items():
-            n.start_soon(portal.run, sleepy_jane)
+            tn.start_soon(
+                portal.run,
+                sleepy_jane,
+            )
 
         await trio.sleep(0.5)
 
@@ -41,4 +44,4 @@ if __name__ == '__main__':
     try:
         trio.run(main)
     except KeyboardInterrupt:
-        pass
+        print('trio cancelled by KBI')
diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py
index fdf54bca..cc4904f8 100644
--- a/tests/test_docs_examples.py
+++ b/tests/test_docs_examples.py
@@ -19,7 +19,7 @@ from tractor._testing import (
 @pytest.fixture
 def run_example_in_subproc(
     loglevel: str,
-    testdir: pytest.Testdir,
+    testdir: pytest.Pytester,
     reg_addr: tuple[str, int],
 ):
 
@@ -81,28 +81,36 @@ def run_example_in_subproc(
 
     # walk yields: (dirpath, dirnames, filenames)
     [
-        (p[0], f) for p in os.walk(examples_dir()) for f in p[2]
+        (p[0], f)
+        for p in os.walk(examples_dir())
+        for f in p[2]
 
-        if '__' not in f
-        and f[0] != '_'
-        and 'debugging' not in p[0]
-        and 'integration' not in p[0]
-        and 'advanced_faults' not in p[0]
-        and 'multihost' not in p[0]
+        if (
+            '__' not in f
+            and f[0] != '_'
+            and 'debugging' not in p[0]
+            and 'integration' not in p[0]
+            and 'advanced_faults' not in p[0]
+            and 'multihost' not in p[0]
+        )
     ],
-
     ids=lambda t: t[1],
 )
-def test_example(run_example_in_subproc, example_script):
-    """Load and run scripts from this repo's ``examples/`` dir as a user
+def test_example(
+    run_example_in_subproc,
+    example_script,
+):
+    '''
+    Load and run scripts from this repo's ``examples/`` dir as a user
     would copy and pasing them into their editor.
 
     On windows a little more "finessing" is done to make
     ``multiprocessing`` play nice: we copy the ``__main__.py`` into the
     test directory and invoke the script as a module with ``python -m
     test_example``.
-    """
-    ex_file = os.path.join(*example_script)
+
+    '''
+    ex_file: str = os.path.join(*example_script)
 
     if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9):
         pytest.skip("2-way streaming example requires py3.9 async with syntax")
@@ -128,7 +136,8 @@ def test_example(run_example_in_subproc, example_script):
                     # shouldn't eventually once we figure out what's
                     # a better way to be explicit about aio side
                     # cancels?
-                    and 'asyncio.exceptions.CancelledError' not in last_error
+                    and
+                    'asyncio.exceptions.CancelledError' not in last_error
                 ):
                     raise Exception(errmsg)
 

From 0d7b3f1ac5ffc2d709491184c70e99910b4028a9 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 18:01:16 -0500
Subject: [PATCH 19/29] Draft some eg collapsing helpers

Inside a new `.trionics._beg` and exposed from the subpkg ns in
anticipation of the `strict_exception_groups=False` being removed by
`trio` in py 3.15.

Notes,
- mk an embedded single-exc "extractor" using a `BaseExceptionGroup.exceptions` length
  check, when 1 return the lone child.
- use the above in a new `@acm`, async bc it's most likely to be composed in an
  `async with` tuple-style sequence block, called `collapse_eg()` which
  acts a one line "absorber" for when the above mentioned flag is no
  logner supported by `trio.open_nursery()`.

All untested atm fwiw.. but soon to be used in our test suite(s) likely!
---
 tractor/trionics/__init__.py |  3 ++
 tractor/trionics/_beg.py     | 59 ++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+)
 create mode 100644 tractor/trionics/_beg.py

diff --git a/tractor/trionics/__init__.py b/tractor/trionics/__init__.py
index c51b7c51..df9b6f26 100644
--- a/tractor/trionics/__init__.py
+++ b/tractor/trionics/__init__.py
@@ -29,3 +29,6 @@ from ._broadcast import (
     BroadcastReceiver as BroadcastReceiver,
     Lagged as Lagged,
 )
+from ._beg import (
+    collapse_eg as collapse_eg,
+)
diff --git a/tractor/trionics/_beg.py b/tractor/trionics/_beg.py
new file mode 100644
index 00000000..37b14238
--- /dev/null
+++ b/tractor/trionics/_beg.py
@@ -0,0 +1,59 @@
+# tractor: structured concurrent "actors".
+# Copyright 2018-eternity Tyler Goodlet.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+'''
+`BaseExceptionGroup` related utils and helpers pertaining to
+first-class-`trio` from a historical perspective B)
+
+'''
+from contextlib import (
+    # bontextmanager as cm,
+    asynccontextmanager as acm,
+)
+
+
+def maybe_collapse_eg(
+    beg: BaseExceptionGroup,
+) -> BaseException:
+    '''
+    If the input beg can collapse to a single non-eg sub-exception,
+    return it instead.
+
+    '''
+    if len(excs := beg.exceptions) == 1:
+        return excs[0]
+
+    return beg
+
+
+@acm
+async def collapse_eg():
+    '''
+    If `BaseExceptionGroup` raised in the body scope is
+    "collapse-able" (in the same way that
+    `trio.open_nursery(strict_exception_groups=False)` works) then
+    only raise the lone emedded non-eg in in place.
+
+    '''
+    try:
+        yield
+    except* BaseException as beg:
+        if (
+            exc := maybe_collapse_eg(beg)
+        ) is not beg:
+            raise exc
+
+        raise beg

From e815dcd3c877f924f5a7eeadcf7e11b4b93e4a22 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 18:30:05 -0500
Subject: [PATCH 20/29] Use `collapse_eg()` in broadcaster suite

Around the test embedded `trio.open_nursery()` calls as expected. Also
tidy up the various nursery var names.
---
 tests/test_task_broadcasting.py | 29 ++++++++++++++++++-----------
 tractor/trionics/_beg.py        |  1 -
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/tests/test_task_broadcasting.py b/tests/test_task_broadcasting.py
index 4a2209eb..b57d63f8 100644
--- a/tests/test_task_broadcasting.py
+++ b/tests/test_task_broadcasting.py
@@ -2,7 +2,9 @@
 Broadcast channels for fan-out to local tasks.
 
 """
-from contextlib import asynccontextmanager
+from contextlib import (
+    asynccontextmanager as acm,
+)
 from functools import partial
 from itertools import cycle
 import time
@@ -15,6 +17,7 @@ import tractor
 from tractor.trionics import (
     broadcast_receiver,
     Lagged,
+    collapse_eg,
 )
 
 
@@ -62,7 +65,7 @@ async def ensure_sequence(
                 break
 
 
-@asynccontextmanager
+@acm
 async def open_sequence_streamer(
 
     sequence: list[int],
@@ -74,9 +77,9 @@ async def open_sequence_streamer(
     async with tractor.open_nursery(
         arbiter_addr=reg_addr,
         start_method=start_method,
-    ) as tn:
+    ) as an:
 
-        portal = await tn.start_actor(
+        portal = await an.start_actor(
             'sequence_echoer',
             enable_modules=[__name__],
         )
@@ -155,9 +158,12 @@ def test_consumer_and_parent_maybe_lag(
         ) as stream:
 
             try:
-                async with trio.open_nursery() as n:
+                async with (
+                    collapse_eg(),
+                    trio.open_nursery() as tn,
+                ):
 
-                    n.start_soon(
+                    tn.start_soon(
                         ensure_sequence,
                         stream,
                         sequence.copy(),
@@ -230,8 +236,8 @@ def test_faster_task_to_recv_is_cancelled_by_slower(
 
         ) as stream:
 
-            async with trio.open_nursery() as n:
-                n.start_soon(
+            async with trio.open_nursery() as tn:
+                tn.start_soon(
                     ensure_sequence,
                     stream,
                     sequence.copy(),
@@ -253,7 +259,7 @@ def test_faster_task_to_recv_is_cancelled_by_slower(
                         continue
 
                 print('cancelling faster subtask')
-                n.cancel_scope.cancel()
+                tn.cancel_scope.cancel()
 
             try:
                 value = await stream.receive()
@@ -371,13 +377,13 @@ def test_ensure_slow_consumers_lag_out(
                                     f'on {lags}:{value}')
                                 return
 
-            async with trio.open_nursery() as nursery:
+            async with trio.open_nursery() as tn:
 
                 for i in range(1, num_laggers):
 
                     task_name = f'sub_{i}'
                     laggers[task_name] = 0
-                    nursery.start_soon(
+                    tn.start_soon(
                         partial(
                             sub_and_print,
                             delay=i*0.001,
@@ -497,6 +503,7 @@ def test_no_raise_on_lag():
                 # internals when the no raise flag is set.
                 loglevel='warning',
             ),
+            collapse_eg(),
             trio.open_nursery() as n,
         ):
             n.start_soon(slow)
diff --git a/tractor/trionics/_beg.py b/tractor/trionics/_beg.py
index 37b14238..843b9f70 100644
--- a/tractor/trionics/_beg.py
+++ b/tractor/trionics/_beg.py
@@ -20,7 +20,6 @@ first-class-`trio` from a historical perspective B)
 
 '''
 from contextlib import (
-    # bontextmanager as cm,
     asynccontextmanager as acm,
 )
 

From 2d18e6a4be798bfbc38a9b699aac5be2f869c54b Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 18:53:13 -0500
Subject: [PATCH 21/29] Match `maybe_open_crash_handler()` to non-maybe version

Such that it will deliver a `BoxedMaybeException` to the caller
regardless whether `pdb` is set, and proxy through all `**kwargs`.
---
 tractor/devx/_debug.py | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/tractor/devx/_debug.py b/tractor/devx/_debug.py
index 884c5aea..c6ca1d89 100644
--- a/tractor/devx/_debug.py
+++ b/tractor/devx/_debug.py
@@ -2287,6 +2287,13 @@ def _set_trace(
     repl.set_trace(frame=caller_frame)
 
 
+# XXX TODO! XXX, ensure `pytest -s` doesn't just
+# hang on this being called in a test.. XD
+# -[ ] maybe something in our test suite or is there
+#     some way we can detect output capture is enabled
+#     from the process itself?
+# |_ronny: ?
+#
 async def pause(
     *,
     hide_tb: bool = True,
@@ -3194,6 +3201,15 @@ async def maybe_wait_for_debugger(
     return False
 
 
+class BoxedMaybeException(Struct):
+    '''
+    Box a maybe-exception for post-crash introspection usage
+    from the body of a `open_crash_handler()` scope.
+
+    '''
+    value: BaseException|None = None
+
+
 # TODO: better naming and what additionals?
 # - [ ] optional runtime plugging?
 # - [ ] detection for sync vs. async code?
@@ -3224,9 +3240,6 @@ def open_crash_handler(
     '''
     __tracebackhide__: bool = tb_hide
 
-    class BoxedMaybeException(Struct):
-        value: BaseException|None = None
-
     # TODO, yield a `outcome.Error`-like boxed type?
     # -[~] use `outcome.Value/Error` X-> frozen!
     # -[x] write our own..?
@@ -3268,6 +3281,8 @@ def open_crash_handler(
 def maybe_open_crash_handler(
     pdb: bool = False,
     tb_hide: bool = True,
+
+    **kwargs,
 ):
     '''
     Same as `open_crash_handler()` but with bool input flag
@@ -3278,9 +3293,11 @@ def maybe_open_crash_handler(
     '''
     __tracebackhide__: bool = tb_hide
 
-    rtctx = nullcontext
+    rtctx = nullcontext(
+        enter_result=BoxedMaybeException()
+    )
     if pdb:
-        rtctx = open_crash_handler
+        rtctx = open_crash_handler(**kwargs)
 
-    with rtctx():
-        yield
+    with rtctx as boxed_maybe_exc:
+        yield boxed_maybe_exc

From a26f817ed1b67ee2758b98edc98f19a2e7402f21 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 3 Mar 2025 18:55:02 -0500
Subject: [PATCH 22/29] Another loosie in the trioisms suite

---
 tests/test_trioisms.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tests/test_trioisms.py b/tests/test_trioisms.py
index 449ddcc2..9f1ccec9 100644
--- a/tests/test_trioisms.py
+++ b/tests/test_trioisms.py
@@ -64,7 +64,9 @@ def test_stashed_child_nursery(use_start_soon):
     async def main():
 
         async with (
-            trio.open_nursery() as pn,
+            trio.open_nursery(
+                strict_exception_groups=False,
+            ) as pn,
         ):
             cn = await pn.start(mk_child_nursery)
             assert cn

From b46a8864499efedfd130524b153ac0fb0d749f4e Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Tue, 4 Mar 2025 13:54:46 -0500
Subject: [PATCH 23/29] Show frames when decode is handed bad input

---
 tractor/msg/_ops.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/tractor/msg/_ops.py b/tractor/msg/_ops.py
index 615ad0c8..dc632217 100644
--- a/tractor/msg/_ops.py
+++ b/tractor/msg/_ops.py
@@ -258,6 +258,9 @@ class PldRx(Struct):
                         f'|_pld={pld!r}\n'
                     )
                     return pld
+                except TypeError as typerr:
+                    __tracebackhide__: bool = False
+                    raise typerr
 
                 # XXX pld-value type failure
                 except ValidationError as valerr:
@@ -799,6 +802,11 @@ def validate_payload_msg(
     roundtripped: Started|None = None
     try:
         roundtripped: Started = codec.decode(msg_bytes)
+    except TypeError as typerr:
+        __tracebackhide__: bool = False
+        raise typerr
+
+    try:
         ctx: Context = getattr(ipc, 'ctx', ipc)
         pld: PayloadT = ctx.pld_rx.decode_pld(
             msg=roundtripped,
@@ -823,6 +831,11 @@ def validate_payload_msg(
             )
             raise ValidationError(complaint)
 
+    # usually due to `.decode()` input type
+    except TypeError as typerr:
+        __tracebackhide__: bool = False
+        raise typerr
+
     # raise any msg type error NO MATTER WHAT!
     except ValidationError as verr:
         try:

From 4aa7e8c022c7774f544a79a0d8ad6930bda1ae69 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 5 Mar 2025 09:49:13 -0500
Subject: [PATCH 24/29] Unpack errors from `pdb.bdb`

Like any `bdb.BdbQuit` that might be relayed from a remote context after
a REPl exit with the `quit` cmd. This fixes various issues while
debugging where it may not be clear to the parent task that the child
was terminated with a purposefully unrecoverable error.
---
 tractor/_exceptions.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tractor/_exceptions.py b/tractor/_exceptions.py
index f90df5fe..249ea164 100644
--- a/tractor/_exceptions.py
+++ b/tractor/_exceptions.py
@@ -22,6 +22,7 @@ from __future__ import annotations
 import builtins
 import importlib
 from pprint import pformat
+from pdb import bdb
 import sys
 from types import (
     TracebackType,
@@ -181,6 +182,7 @@ def get_err_type(type_name: str) -> BaseException|None:
         builtins,
         _this_mod,
         trio,
+        bdb,
     ]:
         if type_ref := getattr(
             ns,

From 84b04639f87eaaccc9812ceb21e9a18f5d0d2afb Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 5 Mar 2025 12:39:16 -0500
Subject: [PATCH 25/29] Bind another `_bexc` for debuggin

---
 tractor/_context.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tractor/_context.py b/tractor/_context.py
index eb66aade..4628b11f 100644
--- a/tractor/_context.py
+++ b/tractor/_context.py
@@ -1561,12 +1561,12 @@ class Context:
                     strict_pld_parity=strict_pld_parity,
                     hide_tb=hide_tb,
                 )
-            except BaseException as err:
+            except BaseException as _bexc:
+                err = _bexc
                 if not isinstance(err, MsgTypeError):
                     __tracebackhide__: bool = False
 
-                raise
-
+                raise err
 
         # TODO: maybe a flag to by-pass encode op if already done
         # here in caller?

From f068782e74875d185a2615bf4602981a46f59ac3 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 10 Mar 2025 11:51:24 -0400
Subject: [PATCH 26/29] Bump to `msgspec>=0.19.0` for py 3.13 support!

---
 pyproject.toml |  4 +---
 uv.lock        | 28 ++++++++++++++++++++++++++--
 2 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index c6eb7532..dd73ed1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,7 +44,7 @@ dependencies = [
   # built-in multi-actor `pdb` REPL
   "pdbp>=1.6,<2", # windows only (from `pdbp`)
   # typed IPC msging
-  "msgspec",
+  "msgspec>=0.19.0",
 ]
 
 # ------ project ------
@@ -75,8 +75,6 @@ dev = [
 # ------ dependency-groups ------
 
 [tool.uv.sources]
-msgspec = { git = "https://github.com/jcrist/msgspec.git" }
-
 # XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`
 # for the `pp` alias..
 # pdbp = { path = "../pdbp", editable = true }
diff --git a/uv.lock b/uv.lock
index 99b9ee23..49eb5437 100644
--- a/uv.lock
+++ b/uv.lock
@@ -126,7 +126,31 @@ wheels = [
 [[package]]
 name = "msgspec"
 version = "0.19.0"
-source = { git = "https://github.com/jcrist/msgspec.git#dd965dce22e5278d4935bea923441ecde31b5325" }
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 },
+    { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 },
+    { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 },
+    { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 },
+    { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 },
+    { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 },
+    { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 },
+    { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 },
+    { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 },
+    { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 },
+    { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 },
+    { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 },
+    { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 },
+    { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 },
+    { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 },
+    { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 },
+    { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 },
+    { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 },
+    { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 },
+    { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 },
+    { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 },
+]
 
 [[package]]
 name = "outcome"
@@ -320,7 +344,7 @@ dev = [
 [package.metadata]
 requires-dist = [
     { name = "colorlog", specifier = ">=6.8.2,<7" },
-    { name = "msgspec", git = "https://github.com/jcrist/msgspec.git" },
+    { name = "msgspec", specifier = ">=0.19.0" },
     { name = "pdbp", specifier = ">=1.6,<2" },
     { name = "tabcompleter", specifier = ">=1.4.0" },
     { name = "tricycle", specifier = ">=0.4.1,<0.5" },

From d14d29ae8c00e0ab49f5932d087f9168bd242f86 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Wed, 19 Mar 2025 10:02:05 -0400
Subject: [PATCH 27/29] Bump up to `pytest>=8.3.5` to match "GH actions"

Ensure it's only for the `--dev` optional deps.
---
 pyproject.toml | 2 +-
 uv.lock        | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index dd73ed1f..b3e9e100 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ dev = [
   # test suite
   # TODO: maybe some of these layout choices?
   # https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
-  "pytest>=8.2.0,<9",
+  "pytest>=8.3.5",
   "pexpect>=4.9.0,<5",
   # `tractor.devx` tooling
   "greenback>=1.2.1,<2",
diff --git a/uv.lock b/uv.lock
index 49eb5437..4c9769fd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -264,7 +264,7 @@ wheels = [
 
 [[package]]
 name = "pytest"
-version = "8.3.4"
+version = "8.3.5"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -272,9 +272,9 @@ dependencies = [
     { name = "packaging" },
     { name = "pluggy" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
 ]
 
 [[package]]
@@ -358,7 +358,7 @@ dev = [
     { name = "pexpect", specifier = ">=4.9.0,<5" },
     { name = "prompt-toolkit", specifier = ">=3.0.50" },
     { name = "pyperclip", specifier = ">=1.9.0" },
-    { name = "pytest", specifier = ">=8.2.0,<9" },
+    { name = "pytest", specifier = ">=8.3.5" },
     { name = "stackscope", specifier = ">=0.2.2,<0.3" },
     { name = "xonsh", specifier = ">=0.19.2" },
 ]

From ceda1e466d06497a3145a4d2c877f33133ea40fc Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Mon, 24 Mar 2025 21:43:54 -0400
Subject: [PATCH 28/29] Drop explicit `tabcompleter` dep, `pdpp` already
 sub-depends on it?

---
 uv.lock | 2 --
 1 file changed, 2 deletions(-)

diff --git a/uv.lock b/uv.lock
index 4c9769fd..e1c409f5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -324,7 +324,6 @@ dependencies = [
     { name = "colorlog" },
     { name = "msgspec" },
     { name = "pdbp" },
-    { name = "tabcompleter" },
     { name = "tricycle" },
     { name = "trio" },
     { name = "wrapt" },
@@ -346,7 +345,6 @@ requires-dist = [
     { name = "colorlog", specifier = ">=6.8.2,<7" },
     { name = "msgspec", specifier = ">=0.19.0" },
     { name = "pdbp", specifier = ">=1.6,<2" },
-    { name = "tabcompleter", specifier = ">=1.4.0" },
     { name = "tricycle", specifier = ">=0.4.1,<0.5" },
     { name = "trio", specifier = ">0.27" },
     { name = "wrapt", specifier = ">=1.16.0,<2" },

From eda48c8021cb243ab78cb98e8ae7c629878c7658 Mon Sep 17 00:00:00 2001
From: Tyler Goodlet <jgbt@protonmail.com>
Date: Tue, 25 Mar 2025 12:54:12 -0400
Subject: [PATCH 29/29] Move bp to-match-comments on same line for py3.13

In the `examples/debugging/restore_builtin_breakpoint.py` i had put the
pattern-comment lines on the line following the `breakpoint()` bc it
seems that's where `pdb` would always "stop" and print the line to
console? So the test would only pass by actually ensuring that in the
`pexpect` capture..

Now on 3.13 it seems that the `pdb` line halting must have been fixed;
it now renders to console the same `breakpoint()` line?
Anyway it works as you'd expect now but **only** on 3.13 so after this
change we might have to adjust the tests to `pytest.xfail()` on earlier
versions.
---
 examples/debugging/restore_builtin_breakpoint.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/examples/debugging/restore_builtin_breakpoint.py b/examples/debugging/restore_builtin_breakpoint.py
index 89605075..b591b0f7 100644
--- a/examples/debugging/restore_builtin_breakpoint.py
+++ b/examples/debugging/restore_builtin_breakpoint.py
@@ -32,8 +32,7 @@ async def main() -> None:
             f'$PYTHONOBREAKPOINT: {pybp_var!r}\n'
             f'`sys.breakpointhook`: {pybp_hook!r}\n'
         )
-        breakpoint()
-        pass  # first bp, tractor hook set.
+        breakpoint()  # first bp, tractor hook set.
 
     # XXX AFTER EXIT (of actor-runtime) verify the hook is unset..
     #
@@ -43,8 +42,7 @@ async def main() -> None:
     assert sys.breakpointhook
 
     # now ensure a regular builtin pause still works
-    breakpoint()
-    pass  # last bp, stdlib hook restored
+    breakpoint()  # last bp, stdlib hook restored
 
 
 if __name__ == '__main__':