`py-multiaddr` already ships the entire tunnel compose/peel
surface and this module was reimplementing it — a raw
`maddr.split('/')` plus index arithmetic, sitting directly under
a comment congratulating itself for not hand-rolling a parser.
Same NIH trap gh #429 existed to close, just one layer up. The
API was linked from gh #443's own 2nd bullet the whole time.
So every cut now goes through the real thing,
| need | API |
| --- | --- |
| isolate the bearer | `.decapsulate_code(P_WG)` |
| per-seg maddrs | `.split()` |
| rejoin a seg tail | `Multiaddr.join()` |
| read the key | `.value_for_protocol('wg')` |
| recompose | `.encapsulate()` |
`.decapsulate_code()` turns out to handle the infix `/wg/` seg
cleanly *because* it cuts on proto-code and never tries to match
an addr value — the key seg has no addr of its own, which was
the exact thing I'd assumed would need bespoke handling.
Deats,
- rename the role fields `inner`/`inner_proto` ->
`overlay`/`overlay_proto`, matching `py-multiaddr`'s
encapsulation model (earlier segs wrap later ones) and #443's
owner table. `inner` collided head-on w/ call-stack `inner`,
where it reads as higher-up + later-called, while here the
encapsulated addr is bound *first* and sits deeper.
- drop `_segments()` and its degraded hand-split path entirely.
W/o the codec there's now one actionable `RuntimeError`
instead of a silent downgrade, superseding the swallow fix in
7d6e7955.
- add `.as_multiaddr()` so callers can stay in `Multiaddr` land;
`.maddr` is now just `str()` of it.
- accept `str|Multiaddr` on the way in.
- carry `bearer_ip`/`overlay_ip` so a v6 stack re-renders as v6
— the old `.maddr` hardcoded `/ip4/` and would silently
mangle it.
- both host scripts follow the rename to `.overlay`.
⚠️ `value_for_protocol('ip4')` on a *full* tunnelled maddr
silently returns the **first** match, i.e. the bearer's host, so
it's only ever called here on an already-peeled sub-maddr.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
it lands" framing in plan-03 and the example README was stale in
both directions: the branch pin is obsolete, yet you still can't
just `pip install multiaddr`.
Deats,
- §3.2's grammar table is now re-verified against the upstream
merge (`f86519da`) rather than only `baudco@wg_support` in a
throwaway venv. Also notes the codec enforces a 32-byte key,
so a truncated one is a `StringParseError` and not a silently
mangled parse.
- §1 says merged-but-unreleased; the still-open work is spec
registration (py-multiaddr#107 + gh #483).
- §3.4 swaps "pin the branch" for the `[tool.uv.sources]` `rev`
pin, and fixes the `_have_wg_maddr_proto()` recipe it
suggested — probing w/ `Multiaddr('/wg/uAAAA')` now ALWAYS
raises bc the codec wants 32B, i.e. that feature-detect would
report `False` even w/ the proto perfectly well known.
- risk table row goes "#108 not merged" -> "merged but
unreleased".
- example README: `uv sync` alone now suffices bc of the pin;
documents the 32B check and points at
`_have_wg_maddr_proto()` as the gate.
The one surviving `baudco` mention is deliberate, it records
where the grammar was *first* verified.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`_segments()` called `Multiaddr(maddr)` purely to validate, then
swallowed every failure under `except Exception: pass`. That was
harmless pre-#108 — w/o a `wg` codec there was nothing to
validate — but now that the codec is pinned in, the swallow is
load-bearing and disabled: a malformed key sails past validation
into `wg8_pubkey()`, which happily emits a corrupt b64 str, and
the returned struct then fails its own `.maddr` round-trip. No
raise, just quietly wrong output.
Deats,
- add `_have_wg_maddr_proto()`, the gate plan-03 already
referenced but which never actually existed. Impl'd as
`protocols.protocol_with_name('wg')` under
`except ProtocolNotFoundError` and cached in a mod global,
same shape as the TIPC plan's `is_tipc_available()`.
- only validate when that gate is `True`, and let
`StringParseError` propagate — a maddr which doesn't parse
must NOT reach `wg8_pubkey()`.
- keep the degraded split for a pre-#108 install, now w/ an
explicit `XXX` naming the validation you give up.
So parsing stays pure but becomes total-or-raises. Our own
`ValueError`s (missing `/wg/` seg, bare tunnel w/o an overlay
ep) are unaffected, as is the `wg(8)` b64 round-trip.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
py-multiaddr#108 (the `/wg/u<key>` maddr proto) merged upstream
on 2026-07-28 as `f86519da`, but ships in no release yet — the
latest `0.2.0` predates it by ~4 months and carries no `wg`
codec at all. So `examples/multihost/wg_lan/` can't parse its
own maddrs off PyPI.
Pinned by `rev` and not `branch` so CI stays reproducible. Note
the lock now records the git source *instead of* the `>=0.2.0`
specifier, i.e. the dep floor above is fully overridden for as
long as this pin lives.
TODO, drop the pin (and bump that floor) the moment a release
carries the codec; the only consumer is the `wg_lan` example
set.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
One record covering all 9 commits on this branch, per the NLNet
generative-AI policy and the existing `ai/prompt-io/claude/`
convention.
Uses diff-ref mode for both the plan docs and the example code
(`git diff main..ng_tpts_planning -- <path>`) rather than
duplicating content already in `git log -p`. Kept verbatim in
the `.raw.md`: the four verified findings (trio's
family-agnostic `SocketStream`/`SocketListener`, the round-trip
table proving `/wg/` is infix, the proto-key `UnwrappedAddress`
rationale, and `setns(2)`'s per-thread reality), since those are
reasoning rather than diffable output.
`## Human edits` records that the steering here was substantial
and mid-session rather than post-hoc: two model claims about wg
maddr semantics were challenged and retracted (incl. in an
already-posted issue comment), and the proto-key +
netns-as-runtime-config framings were human-directed. Also notes
the one model-initiated correction — a pre-publication
self-review that downgraded the `uniffi`/asyncio thesis and the
TIPC duplicate-binder claim to explicitly-flagged assumptions.
Prompt-IO: ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.md
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`tests/test_docs_examples.py` walks `examples/` **recursively**
and subproc-runs every collected file asserting `rc == 0`. Ran
its exact filter against the tree: all 4 of our files were being
collected — including `README.md`, since the filter never checks
the extension, so CI would have literally tried `python
README.md`. These need a real second host + a live `wg` tunnel,
so they can't ever satisfy that gate.
`'multihost' not in p[0]` is already in the test's exclusion
list w/ no dir yet using it, so this is a pure `git mv` — zero
test changes — and it's what the exclusion was plainly there
for. Collection drops 24 -> 20 files, 0 of them ours.
Also records *why* in the two places someone would look before
adding the next one: a callout at the top of the example README
and a note on plan 03's §3.4 deliverables. Anything needing a
second host or live tunnel goes under `examples/multihost/`.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.
- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
`bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
property that re-renders the canonical form, and pure
`mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
rejects #482's inverted suffix form w/ an actionable error and
stays **side-effect free** — `verify_wg_peer()` is a separate,
explicitly impure step the caller composes, never something a
parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
only `addr.inner` into `open_nursery()`/`open_root_actor()`,
which is the whole point — the bearer + key layers are already
established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
branch install line, tunnel setup, and a "what changed vs
#482" section enumerating the corrections.
Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
The prior revision (and gh #482's examples) had it as a suffix,
`/ip4/10.0.11.1/tcp/1616/wg/u<key>`. Wrong: verified against
`baudco/py-multiaddr@wg_support` (py-multiaddr#108) installed in
a throwaway venv, the canonical form is
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
where segs *before* `/wg/` are the **bearer** — the underlay
`(ip, udp-port)` `wg(8)` itself listens on (`ListenPort`), per
the codec docstring's own example — and segs *after* are the
**overlay** ep, the only part we ever bind. The suffix form does
parse, which is why it slipped through, but it's semantically
inverted: overlay addr where the bearer belongs, `tcp` where
wg's `udp` goes, and no overlay ep declared at all.
Records the observed `[p.name for p in m.protocols()]` lists so
the `match` can be written against fact, and replaces the
"composed vs not" framing w/ what's actually the design axis:
three parts, three **owners** — bearer bound by the kernel via
`wg-quick`/`pyroute2`, `/wg/u<key>` bound by nothing (it's an
identity, verified out-of-band), overlay bound by our
`IPCServer` as `.inner`. `_peel_tunnel_segs()` correspondingly
grows a 3rd return, splitting *at* the tunnel seg so nested
tunnels fall out for free.
Also hoists the netns conclusion to the top of §5.3 where it
can't be missed: netns is a **runtime-level config API, not an
actor-app-code one**. It's a spawn/boot-time input alongside
`enable_transports`/`tpt_bind_addrs`, deliberately w/ no
`await actor.enter_netns(...)`, because `setns(2)` neither moves
already-created sockets nor applies beyond the calling thread —
so a mid-life API would silently leave the IPC server bound in
the old ns.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Shape-matching in `wrap_address()` doesn't survive 4 backends and
the plans were papering over it: TIPC's natural unwrapped form is
a `(str, int)`, indistinguishable from `TCPAddress`, and iroh's
is a `(str, str)`, which the *existing* UDS case
(`case (_, filename) if type(filename) is str`) already swallows.
So the contract doc (§1.1) now carries the conclusion as a
**recommended prerequisite for all three backends**: make the
unwrapped form carry an explicit proto-key spelled with the
`multiaddr` protocol name — `('tcp', host, port)`,
`('unix', path)`, `('tipc', stype, inst, scope)`. `wrap_address()`
then collapses from an order-sensitive `match` to
`_address_types[addr[0]]` and the whole collision class stops
existing, while the on-wire form finally agrees w/
`mk_maddr()`/`parse_maddr()` instead of being an independent
invention.
Two consequences spelled out: it's a wire-format change
(`SpawnSpec`, `_root_mailbox`, `_registry_addrs`) + every fixture
+ downstream config, so it wants its own migration commit landed
*before* any new backend; and it's the moment to stop handing raw
tuples to users at all — `Address` becomes the public currency
and `UnwrappedAddress` an internal serialization detail, the same
discipline `ipaddress` uses (you pass `IPv4Address`, never a
4-tuple).
Plan 01 §2.2 is rewritten to match and to explicitly **retract**
its own earlier `('tipc:<stype>:<scope>', instance)` self-tagging
prefix hack — it keeps `wrap_address()` order-sensitive and does
nothing for the iroh/UDS collision, so the doc says don't
resurrect it. Registration checklist item 4 likewise becomes "do
the migration first, then this is a one-line `_address_types`
entry".
Also seeds a `/tipc` multiaddr-spec submission as a follow-up,
mirroring the `wg` track (multiformats/py-multiaddr#107/#108 + gh
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Landing page for `ai/tpt-backends/`: points at the contract spec
as required first reading, tables the 3 plans against their
issues/deps/size, and states the landing order + why.
Deats,
- TIPC first as the cheap proof the table-registration story
generalizes to a genuinely new proto (stdlib-only, and
`trio`'s sock wrappers are family-agnostic).
- `wg` layer-A next since it's deployable-today doc/example work.
- QUIC last, gated on its own prep PR.
- notes that plans 01 and 02 both want the same
`Address.rebind_from_sockname` gate, so whichever lands first
ships it.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #482 + the tunnelled-maddr item of #443. Pushes
back on the framing that `wg` is a tpt: it's transparent to
`socket(2)`, so it belongs as a *bindspace* — a scoped
`@acm`-managed net ctx that an existing L4 tpt binds *inside* —
and it's what finally implements the long-spec'd (never
implemented) `Address.namespace`.
Deats, 3 independently-shippable layers,
- A) declarative: commit #482's examples, teach `parse_maddr()`
the `/…/wg/u<key>` suffix -> a `TunnelledAddress` wrapper whose
`.proto_key`/`.unwrap()` delegate to `.inner` so nothing new
crosses the wire and every existing table lookup keeps working.
- B) swap the `subprocess.run(['sudo', 'wg', 'show'])` shelling
for `pyroute2`. Default to `trio.to_thread` around the sync API
(these are one-shot ops at bind/teardown, never hot-path), w/
sans-io codecs + a trio `AF_NETLINK` sock as the follow-up for
the read paths. Explicitly forbids dragging `trio-asyncio` in.
- C) `open_bindspace()`/`open_netns()`/`open_wg_iface()` `@acm`s
folded w/ an `AsyncExitStack`, + filling in the
`# !TODO, always be ns aware!` placeholder already sitting in
`Endpoint.pformat()`.
Also flags the subtlest bug in the whole thing: `setns(2)` is
*per-thread*, so a `pyroute2` query issued via `trio.to_thread`
lands in the *original* netns. Test-first, per usual.
Further, designs for the generalization (`TunnelSpec` union +
`match` dispatch) while only implementing `wg`+netns, and calls
out `veth`-in-netns as the better *first* one bc it makes a
fully self-contained two-"host" integration test possible w/o
`wg` at all.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #353. Picks `iroh` (the `uniffi` FFI pkg) over
`aioquic`/`quiche` bc node-id addressing + hole-punching + relay
fallback is the whole point; `aioquic` stays documented as the
fallback since ~90% of the adapters here are reusable against a
sans-io core.
Deats,
- the layering: iroh `Endpoint` per actor, `Connection` per peer
(pooled via `trionics.maybe_open_context()`, not a hand-rolled
cache), one bi-stream per `Channel`. 4-byte prefix framing
stays so `MsgpackTransport` is untouched.
- `_uniffi_trio.py`: uniffi only uses `asyncio` as the executor
for its rust-future poll loop, so a ~40-line
`TrioToken.run_sync_soon()` bridge replaces it. Spells out the
real hazards — strong ref on the `ctypes` trampoline, poll-code
propagation, and a *bounded* shielded cancel-drain so a wedged
rust future can't make an actor un-cancellable.
- `IrohAddress` w/ ALPN as the `.bindspace`, the `(str, str)`
unwrapped form's collision w/ the UDS match-case, and why
`get_root()` needs a persisted secret key -> a lazy
`default_lo_addrs()` + a pure-getter/explicit-setter split.
- `QuicMsgStream(trio.abc.HalfCloseableStream)` +
`QuicListener(trio.abc.Listener)`, incl. the exact
EOF/reset/use-after-close semantics `_transport.py` already
match-cases on, and hanging the acceptor tasks off the
existing `Endpoint.listen_tn`.
- a prep-PR boundary: annotation widening, the shared
`rebind_from_sockname` gate and a `tpt_key`-based
`transport_from_stream()` dispatch, all landable w/ tcp/uds as
the only backends.
Further, notes this is our first tpt w/ real transport security
+ peer auth, so an inbound node-id allowlist hook belongs here —
and that it says nothing about the other backends.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #378, the cheapest new backend we can add: it's
stdlib-only (CPython ships `AF_TIPC` + 23 `TIPC_*` consts) and
per the contract doc `trio`'s stream/listener wrappers don't care
about the addr family, so `MsgpackTransport` framing and
`trio.serve_listeners()` are reused verbatim.
Deats,
- `TIPCAddress` as a *service name* `(type, instance)` w/ scope
as the `.bindspace`; `bind()` publishes the singleton
name-range, peers `connect()` by name and the kernel resolves
+ load-balances. I.e. registration/lookup for free, no
registrar in the loop.
- the self-tagging `('tipc:<stype>:<scope>', instance)` unwrapped
form + why it must be match-ordered before `TCPAddress`'s.
- `get_random()` via a blake2b digest of the actor id (there's no
`port=0` analogue) and the silent-crosstalk risk that follows:
TIPC *allows* dup binders and round-robins, so a collision
doesn't `EADDRINUSE`, it cross-talks.
- an `Address.rebind_from_sockname` ClassVar to opt out of
`Endpoint.start_listener()`'s `getsockname()` reconcile, which
for TIPC always returns a port-id, never the bound name.
- the `TIPC_TOP_SRV` topology-service subscription as an `@acm`
yielding a chan of typed name-table events — push-based
register/dereg, the real "end game cluster proto" bit.
- commit sequencing, hard capability gating (`modprobe tipc`;
bare `AF_TIPC` is `EAFNOSUPPORT` on a stock box), CI matrix
notes, risks + follow-up seeds.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
First doc of a new `ai/tpt-backends/` set: the normative
description of what a `tractor` tpt backend *is* as of `main`,
written so the 3 sibling plans (TIPC, QUIC, `wg`) can be worked
independently (by another model/provider) w/o design drift.
Deats,
- the backend duck-type as empirically derived from
`_tcp.py`/`_uds.py`: the `Address` protocol surface, the
mod-level `start_listener()`/`close_listener()` pair and
`Msgpack<Proto>Stream(MsgpackTransport)`.
- the ONE reflection you can't break:
`Endpoint.start_listener()` resolves the tpt mod via
`inspect.getmodule(self.addr)`, so an `Address` type and its
listener fns MUST live in the same mod.
- a 10-item registration checklist (`_address_types`,
`_key_to_transport`, `_addr_to_transport`, `wrap_address()`
match-cases, `TransportProtocolKey`, maddr tables, ..) incl.
the import-time `_default_lo_addrs` trap.
- where the `trio.SocketListener` assumption is *actually*
load-bearing (just the `getsockname()` reconcile) vs. merely
annotated.
- the handshake/discovery invariants a new backend inherits,
dep policy (extras + import-laziness per the #470 boot-latency
budget), `--tpt-proto` harness plumbing and code style.
Also, records a verified finding the plans lean on hard:
`trio.SocketStream`/`SocketListener` are addr-*family* agnostic
— the only ctor checks are "is a trio sock" + `SOCK_STREAM` (+
an `OSError`-suppressed `SO_ACCEPTCONN`) — so any `SOCK_STREAM`
family CPython can make drops into the existing
`trio.serve_listeners()` path unmodified.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Give only the macOS matrix leg two retries so actor/PTY timing
flakes do not strand otherwise-green runs. Linux and Windows remain
strict first-attempt jobs, while persistent macOS failures stay red
after the final visible rerun.
Deats,
- add the pytest-dev-maintained plugin to testing deps
- keep a one-second delay between macOS attempts
- validate the workflow, lock and both observed flaky test areas
Prompt-IO: ai/prompt-io/opencode/20260821T052052Z_3690e43a_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Handle the child RPC leaf as an early return, then leave the root-only
runtime and recursive one-shot flow unindented. Rename the helper for
that behavior and document why RPC namespace lookup requires it to
remain import-addressable at module scope.
Review: PR #484 (goodboy)
https://github.com/goodboy/tractor/pull/484#discussion_r3859956375
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Point `test_tractor_cancels_aio()`'s anti-hang guard at the original
fix commit and the detailed ria-removal analysis plan.
Review: PR #484 (goodboy)
https://github.com/goodboy/tractor/pull/484#discussion_r3859956368
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Release 25 concurrent `to_actor.run()` callers through one local
barrier so their implicit child starts race the first remote error.
Exercise one, five and 25 deterministically placed errorers without
overloading actor-nursery internals.
Validate bounded cancel-on-first teardown, only boxed assertion
relays and empty actor-nursery child/reap maps across Trio and
multiprocessing backends. Clarify this successor's distinction from
`test_nested_multierrors()`, diagram flat-pool error propagation and
align the nearby expected-error comment with its handler.
Review: PR #484 (goodboy)
https://github.com/goodboy/tractor/pull/484#discussion_r3858891778
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Fix five spelling errors in comments and failure text touched by the
one-shot migration: one `propagate`, one `Daemon` and three `directly`
corrections.
Review: PR #484 (GitHub Copilot)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025348921
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Add the PR #484 towncrier fragment for removing
`ActorNursery.run_in_actor()`, `Portal.wait_for_result()` and
`Portal.result()`.
Point callers to `to_actor.run()`, `Portal.run()` or
`Portal.open_context()` according to task ownership and dialog shape.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Inspect the exception emitted by the concurrent error fan-out instead
of accepting any `RemoteActorError` or `BaseExceptionGroup`.
Require each non-cancellation leaf to box `AssertionError`, allow one
or two relays for cancel-on-first timing and require both child relays
when no cancellation leaf accompanies the group.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Open one linked sleeping context in each daemon and wait for every
`StartAck` before `test_cancel_via_SIGINT_other_task()` reports
startup.
This restores the legacy test's active remote-task cancellation
target instead of proving SIGINT teardown only against idle actor
runtimes.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Give each background subscriber runner its own teardown event and
suppress only `ContextCancelled` relayed by the root actor after that
portal's explicit cancellation begins.
Let generic remote errors, foreign cancellation and cancellation
before teardown escape the local task nursery so the test cannot pass
after a subscriber fails unexpectedly.
Review: PR #484 (GitHub Copilot and OpenCode)
https://github.com/goodboy/tractor/pull/484#discussion_r3858426546
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Replace `test_dynamic_pub_sub()`'s fixed startup sleep with an RPC
activity probe in the publisher actor. Track the publisher task and
wait until every launched consumer has installed its first
subscription before raising the user cancellation exception.
This keeps slow spawn backends from passing the regression by
cancelling actors which never reached the streaming workload.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Wrap the documented publisher stream in `try/finally` and explicitly
cancel its `start_actor()` daemon. Closing `open_stream_from()` owns
only the remote stream task, so actor-nursery exit otherwise waits on
the still-running actor indefinitely.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Keep the `@pub` docstring example's remote target namespace
addressable by passing the module-level function directly to
`Portal.open_stream_from()`.
Forward the topic and task-name inputs as RPC kwargs instead of
wrapping the target in a `functools.partial` object that resolves to
the wrong namespace path.
Review: PR #484 (OpenCode)
https://github.com/goodboy/tractor/pull/484#pullrequestreview-5025383596
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
PR #481 made target inputs positional and reserved keywords for
actor placement/runtime controls. PR #484 still forwarded target
kwargs, so tests and examples failed local signature binding after
the rebase.
Deats,
- bind named target inputs with `functools.partial()`
- keep placement, naming and runtime controls as direct keywords
- reject invalid target calls locally before actor startup
- require linked one-shots to raise one direct `RemoteActorError`
- doc linked context execution and per-child process reaping
Prompt-IO: ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Fold the endeavour's resolution into the plan doc + log the
session per prompt-io policy,
- `ria_nursery_removal_plan.md`: RESOLVED section — migrate
everything, remove the API; the migration-pattern table
(blocking / fire-and-forget / fan-out / collect-don't-cancel
/ mutual-rendezvous), the semantic deltas (cancel-on-first +
`collapse_eg()` chain collapse vs the old teardown-reap BEG),
the excision inventory and the structural dissolution of the
reap-hang class.
- adds the `to_actor.open_one_shot()` follow-up sketch: an
`@acm` + private task-nursery over the existing blocking
`run()` — done-`trio.Event` as a result memo (NOT a
cancel-relay), no `Portal` in the iface, errors always
propagate at scope exit; zero `_supervise` coupling.
- prompt-io entry `20260706T172818Z_ad42871e` (+ raw diff-ref
companion) covering commits `d01a2123..ad42871e`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Convention sweep (user req): all `tractor.open_nursery()`
bindings in test + example code use `an: ActorNursery` (`n`,
`nursery` + several tractor-nurseries confusingly named `tn`
are renamed); `trio.open_nursery()` bindings stay `tn` (incl.
`concurrent_actors_primes.py`'s inner trio nursery, renamed
`n` -> `tn` to match).
Purely mechanical, function-scoped renames — prose "nursery"/
"an" in docstrings/comments untouched; func-arg kwargs like
`portal.run(func, n=value)` untouched.
Gate: renamed test modules green on `trio`; full debugger suite
(28p/6s) + example-runner (21p) green.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The `test_trynamic_trio` + `a_trynamic_first_scene.py` migration
to paired `to_actor.run()` one-shots carries a race the legacy
`run_in_actor()` shape never had: donny + gretchen each
`wait_for_actor()` (then DIAL) the *other*, but a one-shot is
reaped the instant its own hello returns — so the slower peer
can resolve the winner's registry entry and connect to an
already-dead sockaddr -> `ConnectionRefusedError` boxed as a
`RemoteActorError` (or a reg-wait `TooSlowError`), flaking
~1-in-3 standalone runs.
Mutual-rendezvous peers must OUTLIVE both dialogs, so pin the
lifetimes explicitly: `start_actor()` both as daemons, run both
hellos concurrently via bg `Portal.run()` tasks, then reap with
`an.cancel()` only after the task-nursery joins. (The legacy
teardown-reap provided this pinning implicitly — one of the
few places its semantics were ever actually relied upon.)
Gate: `-k trynamic` standalone x8 green (was flaking); full
`test_registrar` module + the example-runner green.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The `_pubsub.pub` decorator's usage example predates several API
generations: ancient positional-arg-order `run_in_actor()` (a
missing `await` too) plus the deprecated `portal.result()` — and
`run_in_actor()` never allowed streaming funcs anyway. Show the
canonical `start_actor()` + `Portal.open_stream_from()`
consumption instead (#477 removal sweep).
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The 8-page docs sweep of the #477 removal, ahead of the API's
excision,
- `start/quickstart.rst`: the first-actor-tree walkthrough now
narrates the (migrated) `to_actor.run()` example — no portal
in hand until the daemon section introduces `start_actor()`.
- `guide/spawning.rst`: the one-shot section becomes
`to_actor.run()` (blocking call, placement opts, "built on the
primitives" note); lifetime/teardown rules update — one-shots
never make it to nursery exit since each is reaped inside its
own call.
- `guide/rpc.rst`: the `wait_for_result()` section (an API that
dies with the reap cluster, incl. the `NoResult` sentinel)
becomes a `to_actor.run()` one-shot section.
- `api/core.rst`: drop `run_in_actor`/`wait_for_result` from the
autodoc member lists, drop the `Portal.result()` deprecation
note, add a "One-shot task actors" `tractor.to_actor.run`
autodoc section.
- `guide/{asyncio,context,cancellation,parallelism}.rst`:
mention swaps to the successor API.
Gate: `make -C docs html` builds clean; `to_actor.run` autodoc
renders in `api/core.html`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The known-flaky dynamic pubsub test's 3 fire-and-forget spawn
sites (#477 removal),
- the forever-streaming `publisher` + N `consumer` one-shots now
bg-schedule as `to_actor.run(fn, an=n)` tasks in a local `trio`
task-nursery (`publisher`'s rendezvous name still derives from
`fn.__name__`).
- the simulated user-cancel raise (`KeyboardInterrupt` /
`TooSlowError` params) cancels the task-nursery, each one-shot
reaping its subactor via `to_actor.run()`'s shielded
`Portal.cancel_actor()`; `_run_and_match()`'s existing
`BaseExceptionGroup.split()` walk covers the (possibly nested)
relay shapes unchanged.
- spawns now issue concurrently rather than sequentially —
comment on the fork-backend budget updated to match.
Gate: both params x4 runs green on `trio` + x1 on `mp_spawn`;
full module green.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Final `test_cancellation.py` group of the `run_in_actor` removal
(#477) — cancel-mechanics tests, so clean conversions,
- `test_cancel_via_SIGINT_other_task`: the 3 keep-alive
`run_in_actor(sleep_forever)` one-shots become plain
`start_actor()` daemons (an idle daemon needs no "main" task,
and no longer shares a single dup'd `namesucka` name).
- `spawn_sub_with_sync_blocking_task`: the middle layer's spawn
becomes a blocking `to_actor.run(spin_for, an=an)` which parks
awaiting the sync-sleeping grandchild's result until cancelled
from above.
- `test_cancel_while_childs_child_in_sync_sleep`: the
fire-and-forget middle-actor spawn becomes a bg
`to_actor.run()` task in a local task-nursery; the root's
`assert 0` cancels it, driving the same
graceful-cancel-then-zombie-reap cascade on the sync-blocked
grandchild. The `man_cancel_outer` xfail param is unchanged.
Zero live `run_in_actor()` call-sites remain in this suite.
Gate: full `test_cancellation.py` module green on both `trio`
(18p/1xf) + `mp_spawn` (18p/1xf).
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Third `test_cancellation.py` group of the `run_in_actor` removal
(#477),
- `spawn_and_error` fans out each level's erroring one-shots as
concurrent `to_actor.run(fn, an=an)` tasks in a local `trio`
task-nursery (recursing per spawner subactor), as does the
test-body's top-level spawner loop.
- the deterministic exact-breadth nested-BEG shape dies with the
legacy teardown-reap: each level now groups whatever subset of
sub-tree errors relay before the first one's cancel wins, and
a single-member group gets unwrapped by the runtime's own
`collapse_eg()` at every actor boundary — so a fully-raced
tree relays a bare `RemoteActorError` chain.
- loosen the shape walk accordingly: accept a lone
`RemoteActorError` or a 1..breadth group whose members box
`ExceptionGroup` (multi-relay), `AssertionError` (collapsed
leaf chain), `RemoteActorError` (re-boxed collapsed chain) or
`BaseExceptionGroup` (runtime reap-deadline `Cancelled`
upgrade); fold the windows-only tolerances into the same walk.
- raced sibling `trio.Cancelled`s are now ABSORBED by the
task-nursery instead of landing in the group, so the MTF
shape-mismatch xfail should consistently xpass — note added to
drop the marker once CI confirms.
- add an `else: pytest.fail()` so a silently-clean tree can no
longer pass.
Gate: both depths green on `trio` (10 consecutive runs) +
`mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
`test_nested_multierrors`'s backend/depth budget `match` only
carries arms for the `trio` + `main_thread_forkserver` spawn
backends, so running under any other (e.g. `mp_spawn`) leaves
`timeout` unbound and crashes with an `UnboundLocalError` at the
headroom-scaling below. Add default per-depth arms riding the MTF
budgets (same per-spawn round-trip cost class).
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Second `test_cancellation.py` group of the `run_in_actor` removal
(#477),
- one-shot subactors now run as concurrent `to_actor.run(fn,
an=an)` tasks in a local `trio` task-nursery, so their errors
raise WHILE the actor-nursery block is open (vs the legacy
teardown-reap) and the first error cancels sibling one-shots.
- wrap the task-nursery in `collapse_eg()` so the deterministic
single-error cases still surface a bare `RemoteActorError`.
- loosen the group-shape assertion: the relay-vs-cancel race
populates anywhere from 1 to `num_actors` `RemoteActorError`s
(the exact-`num_actors` BEG was `run_in_actor`'s
reap-all-at-teardown); group members are always
`RemoteActorError` now since sibling `trio.Cancelled`s are
absorbed by the task-nursery.
- move the daemon-portal call loop inside the task-nursery body
so the sleep-forever one-shot case is cancelled by the daemon
error raise.
- rename the `*run_in_actor*` param ids to `*one_shot*`.
Gate: 6 passed on both `trio` + `mp_spawn` backends.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Append two sections to the ria-removal plan capturing the
2026-07-02 hang episode + the resulting design pivot.
Regression writeup: the full-suite hang on
`test_tractor_cancels_aio` root-caused to the step-A reaper
hoist (`5cd190c5`), not the B2 handler merge. The happy-path
`_reap_ria_portals()` parks unbounded on `wait_for_result()`
after a user `portal.cancel_actor()`; the old spawn-backend
reaper raced `soft_kill()`'s scope-cancel, the hoist dropped
it. Records the `proc.poll()` death-watch fix + why poll (not
the event `wait_func`) bc `soft_kill` already awaits
`proc.sentinel` (a 2nd `wait_readable` -> `BusyResourceError`).
Pause writeup: user's insight that the hoist landed in the
wrong scope — result-waiting belongs in the `to_actor`
one-shot scope (`_invoke_in_subactor()`), beside `an` + a
local task-nursery + cancel-scope, where bounding the wait is
trivial + the hang dissolves. So the poll fix is likely
SUPERSEDED (flagged do-not-land); the anti-hang guard commit
(`d1fb4a1a`) stays red-first per the failing-test convention.
(this patch was generated in some part by `claude-code` using
`claude-opus-4-8` (`anthropic`))
First group of the `test_cancellation.py` `run_in_actor` removal
(#477),
- `test_remote_error` -> blocking `to_actor.run()` (single erroring
one-shot; a bad-arg `TypeError` still relays as a
`RemoteActorError`).
- `test_multierror` -> concurrent fan-out via
`gather_contexts([p.open_context(assert_err_ctx) ...])` over
`start_actor()` portals. NB `gather_contexts` is cancel-on-first
so the 2nd errorer is usually cancelled before relaying its own
exc and the pair collapses to a single `RemoteActorError` (vs the
legacy reap-all-at-teardown `BEG`-of-N) — the assertion now
accepts either shape.
- delete `test_multierror_fast_nursery` — a 25-actor stress test of
`run_in_actor`'s teardown-reap; no analogous surface under the
`to_actor` fan-out.
- add an `assert_err_ctx` `@context` shim for the `open_context`
fan-out.
Remaining `test_cancellation` groups (some_cancels_all, nested,
SIGINT, sync-blocking) still on `run_in_actor` — ported next.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Two sites migrated (#477 removal),
- `test_trynamic_trio`: donny + gretchen each wait on the *other*
to register, so they must run CONCURRENTLY — was two
non-blocking `run_in_actor()`s awaited after; now two
`to_actor.run()` one-shots scheduled into a local `trio`
task-nursery.
- the unregister-on-cancel cluster test: its non-streaming branch
spawned `run_in_actor(trio.sleep_forever)` purely to keep each
subactor alive + registered — a `start_actor()` daemon does that
without a "main" task, so the spawn loop collapses to the same
`start_actor()` the streaming branch already used.
Suite: 16 passed.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
`test_multi_actor_subs_arbiter_pub` used `run_in_actor()` to spawn
two forever-ish subscriber actors and hold their portals for a
later `cancel_actor()` (its `.result()` was commented out exactly
because `subs()` never cleanly returns). That deferred-spawn +
cancel shape isn't a blocking `to_actor.run()`, so convert to the
successor primitives (#477 removal),
- `start_actor()` per subscriber — keeps the portal for the
existing `cancel_actor()` teardown,
- run `subs()` on each via a background `Portal.run()` task in a
local `trio` nursery so both subscribe concurrently with the
test's `wait_for_actor` / topic checks,
- each bg runner swallows the `RemoteActorError`/`ContextCancelled`
that `cancel_actor()` relays; a trailing `tn.cancel_scope.cancel()`
drops any lingering runner.
Suite: 8 passed.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Migrate all 4 sites to blocking `tractor.to_actor.run()` (#477
removal),
- rename the two API-named tests to `test_to_actor_run_*`
(`same_func_in_child`, `can_skip_parent_main_inheritance`) —
they exercise the same spawn / `inherit_parent_main` path via
the successor API.
- the recursive `spawn()` helper drops its white-box
`an._children` / portal-`_peers` asserts (which probed
`run_in_actor`'s portal + nursery-tracking internals);
`to_actor.run()` returns the result and reaps internally, so
keep the user-facing `result == 10` check.
- `test_most_beautiful_word` drops the 2nd `wait_for_result()`
(the legacy result-cache re-fetch) — `to_actor.run()` delivers
the value once, no cache.
Suite: 9 passed.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Sole call-site: `run_in_actor(sleep_back_actor, ...)` ->
blocking `tractor.to_actor.run(..., an=n, ...)` (#477 removal).
The RPC-callback subactor is awaited in-caller instead of
reaped at nursery teardown; `name=`/`enable_modules=` map to
`to_actor.run()`'s same-named params, the rest to `**fn_kwargs`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Sole call-site: an inlined `run_in_actor(...).result()` ->
blocking `tractor.to_actor.run(fn, an=an, ...)` (#477 removal).
Behaviour identical — the one-shot's result/error is awaited
in the caller's task rather than reaped at nursery teardown;
the enclosing `move_on_after` still cancels the sub in the
`error_in_child=False` case.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
First test-file of the #477 `.run_in_actor()` removal (blocking
`to_actor.run()` is the successor; the legacy non-blocking one-shot
is dropped, not replaced). All 9 call-sites migrated,
- blocking result/error/streaming-result tests -> `to_actor.run(fn,
an=an, ...)`; the "streaming" ones stream aio<->trio INSIDE the
subactor so the caller only awaits the final result.
- forever-task + cancel tests (`test_tractor_cancels_aio`,
`test_trio_cancels_aio`) -> `start_actor()` +
`Portal.open_context()` + cancel — can't block on a
never-returning task. Adds a small `sleep_forever_aio_ctx`
`@context` shim.
- greens the red `test_tractor_cancels_aio` anti-hang guard from
the prior commit: under the correctly-scoped API the wait is
bounded by the caller's cancel scope, so the hang is structurally
gone — not patched.
Suite: 34 passed, 2 xfailed (trio backend).
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Wrap `test_tractor_cancels_aio`'s `main()` in a
`trio.fail_after(9 * cpu_perf_headroom())` so a wedged remote
runtime can't hang the test forever. This is the blessed
anti-hang guard here bc `pytest-timeout`'s global cap is
intentionally off (it breaks `trio` under the fork backends,
per the `pyproject` NOTE).
The cap is generous + CPU-headroom-scaled bc it's an anti-hang
guard, not a perf assertion. Motivated by the
`._ria_nursery`-removal regression where a wedged ria-reaper
once hung this exact test indefinitely.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Step B2 of the `._ria_nursery` removal (issue #477; see
`ai/conc-anal/ria_nursery_removal_plan.md`). With the 2ndary
nursery gone (step B), the two nested error handlers in
`_open_and_supervise_one_cancels_all_nursery` collapse to one,
- the outer `except (Exception, BaseExceptionGroup,
trio.Cancelled)` existed to catch errors bubbling from the
old `._ria_nursery.__aexit__` reaper-group; that nursery no
longer exists.
- trace shows the outer handler's `raise` was already DEAD: the
inner handler records `errors[uid]` as its first action, so
`errors` is always non-empty by the time anything could reach
the outer handler, and the `finally`'s raise-from-`errors`
always superseded the outer `raise`.
- so fold both into a single `except BaseException as
_scope_err` guarding the lone daemon nursery; the `finally`
(unchanged) still raises the collected `errors` as a single
exc or `BaseExceptionGroup`.
- drop the now-unused `outer_err`/`inner_err` locals.
Behaviour-preserving (net ~30 lines lighter); the big diff is
the one-level de-indent of the handler body. The two remaining
`maybe_wait_for_debugger()` guards collapse to the single
pre-teardown wait.
Prompt-IO: ai/prompt-io/claude/20260702T222544Z_9201a2ed_prompt_io.md
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code