From 63ab7c986b5f6ad389397ab733acde211d4b86a8 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 22 Apr 2026 19:01:27 -0400 Subject: [PATCH] Reset post-fork `_state` in forkserver child MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os.fork()` inherits the parent's entire memory image, including `tractor.runtime._state` globals that encode "this process is the root actor" — `_runtime_vars`'s `_is_root=True`, pre-populated `_root_mailbox` + `_registry_addrs`, and the parent's `_current_actor` singleton. A fresh `exec`-based child starts with those globals at their module-level defaults (all falsey/empty). The forkserver child needs to match that shape BEFORE calling `_actor_child_main()`, otherwise `Actor.__init__()` takes the `is_root_process() == True` branch and pre-populates `self.enable_modules`, which then trips `assert not self.enable_modules` at the top of `Actor._from_parent()` on the subsequent parent→child `SpawnSpec` handshake. Fix: at the start of `_child_target`, null `_state._current_actor` and overwrite `_runtime_vars` with a cold-root blank (`_is_root=False`, empty mailbox/addrs, `_debug_mode=False`) before `_actor_child_main()` runs. Found-via: `test_subint_forkserver_spawn_basic` hitting the `enable_modules` assert on child-side runtime boot. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/spawn/_subint_forkserver.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tractor/spawn/_subint_forkserver.py b/tractor/spawn/_subint_forkserver.py index 23322084..bf3f3789 100644 --- a/tractor/spawn/_subint_forkserver.py +++ b/tractor/spawn/_subint_forkserver.py @@ -548,6 +548,32 @@ async def subint_forkserver_proc( # every spawn — it's module-level in `_child` but # cheap enough to re-resolve here. from tractor._child import _actor_child_main + # XXX, fork inherits the parent's entire memory + # image — including `tractor.runtime._state` globals + # that encode "this process is the root actor": + # + # - `_runtime_vars['_is_root']` → True in parent + # - pre-populated `_root_mailbox`, `_registry_addrs` + # - the parent's `_current_actor` singleton + # + # A fresh `exec`-based child would start with the + # `_state` module's defaults (all falsey / empty). + # Replicate that here so the new child-side `Actor` + # sees a "cold" runtime — otherwise `Actor.__init__` + # takes the `is_root_process() == True` branch and + # pre-populates `self.enable_modules`, which then + # trips the `assert not self.enable_modules` gate at + # the top of `Actor._from_parent()` on the subsequent + # parent→child `SpawnSpec` handshake. + from tractor.runtime import _state + _state._current_actor = None + _state._runtime_vars.update({ + '_is_root': False, + '_root_mailbox': (None, None), + '_root_addrs': [], + '_registry_addrs': [], + '_debug_mode': False, + }) _actor_child_main( uid=uid, loglevel=loglevel,