Commit Graph

87 Commits (e4dc8d240624ee5eb43122a26285e27ef8613ce3)

Author SHA1 Message Date
Gud Boi e4dc8d2406 Sketch first-child `wgman` supervisor
Add a candidate Layer-C architecture where a private, eagerly
spawned manager owns pyroute2 and tunnel provisioning for a simple
WG-enabled actor tree.

Deats,
- overlap manager reconciliation with sibling process startup
- contain `AsyncWireGuard` in an infected-asyncio child
- limit requests and capabilities by bindspace security domain
- define readiness, crash, restart and teardown semantics
- retain pre-provisioned and multi-manager escape hatches

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:29 -04:00
Gud Boi f671b589b7 Read WireGuard iface keys through `pyroute2`
Add async `read_wg_pubkey()` and `read_wg_peers()` helpers which
offload `WireGuard.info()` calls to a `trio` worker thread.

Deats,
- add the Linux-only `wg` extra and pin `pyroute2`
- pass `flags=0` so a read never creates a named netns
- normalize multipart replies, validate keys and de-dup peers
- always close the netlink client, including error paths
- test thread offload, netns forwarding and client cleanup

Prompt-IO: ai/prompt-io/opencode/20260821T233204Z_5d92595f_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:29 -04:00
Gud Boi 5b606ba1e1 Emit canonical tagged addresses
- Make `TCPAddress.unwrap()` emit `('tcp', host, port)` and
  `UDSAddress.unwrap()` emit `('unix', path)` while retaining the
  compatibility readers from the preceding change.

- Pass concrete TCP fields to Trio, compose multiaddrs from tagged
  values, and let `SpawnSpec` carry protocol-specific tuple shapes
  for validation by `wrap_address()`.

- Compare runtime, registry, bind, and tunnel addresses through
  canonical serialized forms and cover both TCP and UDS operation.

Prompt-IO: ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 68addd86fb Decode tagged transport addresses
- Define canonical `tcp` and `unix` tuple shapes while retaining
  legacy pair aliases as the emitted `UnwrappedAddress`.

- Dispatch tagged tuple/list payloads explicitly, accept `uds` as a
  Unix input alias, and preserve legacy TCP, UDS, and native IPv6
  readers.

- Cover tag aliases, msgpack-style lists, legacy payloads, and IPv6
  socket addresses before switching writers.

Prompt-IO: ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi a753b1e89e Model bindspaces as scoped capabilities
Separate serializable bindspace declarations from live namespace
identity, FDs, ownership and teardown resources.

Require child namespace entry during spawn bootstrap, before actor
runtime initialization, then distinguish listen/dial provisioning and
owned/borrowed cleanup without encoding operation role into maddrs.

Prompt-IO: ai/prompt-io/opencode/20260820T021516Z_dfad66a0_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi f44dad362d Peel tunnels before `Endpoint` binding
Carry tunnel declarations through listener configuration, then strip
them immediately before constructing transport endpoints.

Also allocate random listener addresses from a contacted registry's
overlay, and prove a real TCP listener never stores the wrapper while
the source declaration retains its bindspace metadata.

Prompt-IO: ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 9c4b3a60ee Peel tunnels before `Channel` connects
Retain tunnel annotations through address declaration, then hand only
the bindable overlay to exact-type transport lookup and dialing.

Broaden `Channel.from_addr()` and `_connect_chan()` inputs accordingly,
and cover plain plus tunnelled TCP dispatch arguments.

Prompt-IO: ai/prompt-io/opencode/20260819T213144Z_f81fc5e5_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 78d6f3b2bc Use discovery's `wg` parser in examples
Drop the example-local address struct and hand-rolled single-tunnel
parser now that discovery owns the production implementation.

Keep only the explicit `wg(8)` peer probe in the multihost helper,
and update the examples and plan for nested parsing, packaged codec
dependencies and tractor-owned bindspace provisioning.

Prompt-IO: ai/prompt-io/opencode/20260818T075031Z_dd02c7c0_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 68ca651f67 Support nested `wg` maddrs
Teach discovery to preserve WireGuard bearer and identity metadata
around a bindable TCP overlay.

Deats,
- encode `wg(8)` keys as strict 32-byte multibase values
- peel nested stacks with `Multiaddr.decapsulate_code()` and compose
  them with `.encapsulate()` instead of splitting strings
- integrate wrappers with `parse_maddr()`, `mk_maddr()`,
  `wrap_address()` and `parse_endpoints()`
- pin the unreleased py-multiaddr#108 codec in package metadata
- cover exact round trips, nesting, bad grammar and missing codecs

Prompt-IO: ai/prompt-io/opencode/20260818T075031Z_dd02c7c0_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 41e7bb61c6 Add `TunnelledAddress` wrapper primitives
Introduce the first layer-A address type from the `wg` bindspace
plan without treating a transparent tunnel as a `MsgTransport`.

Deats,
- add frozen `WGTunnelSpec` and `TunnelledAddress` structs which
  delegate proto identity, bindspace, validity and wire
  serialization to their overlay
- add `strip_tunnels()` and `tunnels_of()` for nested wrappers
- recognize wrappers in `is_wrapped_addr()` while keeping them out
  of `_address_types`
- cover delegation, namespace fallback and nested peeling semantics

Also,
- widen `Address.namespace` ids for named netns
- export the new discovery API
- clarify that tractor's layer-C bindspace lifecycle may provision
  the kernel-owned bearer without making it a `MsgTransport`

Prompt-IO: ai/prompt-io/opencode/20260818T021729Z_d9a6e2e9_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-29 22:00:16 -04:00
Gud Boi 4730b4c9cf Retract the hand-rolled tunnel peeler from plan-03
§3.2 specced a pure fn `_peel_tunnel_segs(proto_names) ->
(bearer_names, tunnel_specs, overlay_names)` to split a maddr at
its tunnel seg. It should never be written: `py-multiaddr` ships
that whole surface already and the plan simply missed it, even
though gh #443's 2nd bullet links the README sections in
question.

Replaced w/ a ⚠️ CORRECTION carrying the verified API table
(`.decapsulate_code(P_WG)` for the bearer, `.split()`/`.join()`
for a seg tail, `.value_for_protocol()` to read a value,
`.encapsulate()` to recompose) plus *why* it works on an infix
`/wg/` seg: the cut is by proto-code, never by matching an addr
value, and the key seg has no addr of its own.

Also,
- adopt `bearer`/`overlay` as the role names throughout, and say
  plainly why not `inner`/`outer` — the call-stack reading of
  "inner" is the exact opposite of the encapsulation one.
- warn that `value_for_protocol('ip4')` on a full tunnelled
  maddr silently yields the *bearer's* host; only call it on a
  peeled sub-maddr.
- note nesting (wg-in-wg) falls out of `.decapsulate_code()`
  cutting at the *last* occurrence, so peel repeatedly rather
  than recursing through a bespoke splitter.
- `mk_maddr()` for `TunnelledAddress` is `.encapsulate()`
  composition, not `str` building.
- README: drop the "degrades to a plain segment split" para,
  since that path is gone — no codec now means one actionable
  raise.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-29 22:00:06 -04:00
Gud Boi d83e9c90d7 Update `wg` docs for the merged py-multiaddr#108
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 6e2edbd0d7 Log prompt-io for the tpt-backend planning arc
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 247bf5be15 Move the `wg_lan` examples under `examples/multihost/`
`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`))
2026-08-29 22:00:06 -04:00
Gud Boi 8f1e69693f Fix the `wg` maddr grammar, `/wg/` is *infix*
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`))
2026-08-29 22:00:06 -04:00
Gud Boi c6267d08a2 Proto-key the unwrapped-addr form in the plans
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 048b112a4c Index the tpt-backend plans w/ a README
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 93aa1e25af Add `wg`-as-nested-bindspace plan doc
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 8f525d20e9 Add `QUIC`-via-`iroh` tpt-backend plan
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 6a46f1f0f9 Add `TIPC` tpt-backend impl plan
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 7c8dd986da Add the `.ipc` tpt-backend contract spec
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`))
2026-08-29 22:00:06 -04:00
Gud Boi 6429e6a515 Reject concurrent `BroadcastReceiver.receive()` calls
Two tasks could receive through one `BroadcastReceiver` and race its
single `_wait_scope`. A later call could replace the scope, leaving
the first blocked through `.aclose()`.

Track the active `trio.lowlevel.Task` per receiver and reject an
overlapping call with `trio.BusyResourceError`. Include the owner's
task name and repr in the error, then clear ownership in `finally`.

Define each receiver as one logical subscription cursor. Concurrent
consumers must use separate `.subscribe()` receivers so each retains
an independent cursor and lag policy. Strict mode reads every retained
value or raises `Lagged`; `raise_on_lag=False` explicitly permits
drops.

Cover the regression with a checkpoint loop synchronized on
`recv_ready`; explain the interleaving inline, assert the error names
the active consumer, then release the source and prove the original
receive remains usable.

Prompt-IO: ai/prompt-io/opencode/20260828T014247Z_d06ee34b_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 22:11:55 -04:00
Gud Boi d06ee34b83 Clean broadcast cancellation diagnostics
Bound `BroadcastState.cancelled` entries to receiver progress,
terminal state and resource lifetime instead of retaining completed
`Task`s indefinitely.

Deats,
- make EOC durable so awakened peers never re-enter a closed source.
- close root broadcasters during explicit `MsgStream` and
  `LinkedTaskChannel` teardown without re-entrant EOC closure or
  breaking `MsgStream.aclose()` overrides.
- reject non-positive fan-out retention capacity before constructing
  an unusable zero-length queue.
- cover child/root cancellation cleanup, terminal peer wakeups,
  wrapper teardown, subclass compatibility and zero-buffer rejection.

Prompt-IO: ai/prompt-io/opencode/20260813T181901Z_a2e0df4b_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi ff4391669b Expose stream subscriber lag policy
`MsgStream.subscribe()` and `LinkedTaskChannel.subscribe()` omitted
`BroadcastReceiver.raise_on_lag`, forcing downstream consumers to
mutate a private receiver attribute when overruns were acceptable.

Add `raise_on_lag` to both public wrappers. The first subscription
sets the irreversible root broadcaster's policy, while every child
selects its own strict or warn/drop/resume behavior independently.

Document both fan-out APIs. Cover policy forwarding plus real IPC
and infected-asyncio paths.

Prompt-IO: ai/prompt-io/opencode/20260812T213117Z_51185487_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi a5259a8ee4 Isolate `BroadcastReceiver.aclose()` wakeups
Closing any subscriber set the shared `recv_ready` event, even when
another receiver owned the source read. Waiting peers then looped
until an idle source produced another value.

Give each receiver private source-read and peer-wait cancellation
scopes. Closing a waiting peer interrupts only that peer. Closing
the source owner discards post-close source outcomes and then wakes
peers for a clean ownership handoff.

Keep outer task cancellation as `trio.Cancelled`; only explicit
receiver close maps either private scope's cancellation to
`ClosedResourceError`. Assert that scope cancellation implies the
receiver is closed and document the source-owner key check.

Prompt-IO: ai/prompt-io/opencode/20260812T150027Z_c2a6ccef_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi d6da0e7a22 Wake broadcast peers on shared receive failures
Only `EndOfChannel` and direct cancellation woke tasks waiting
behind the subscriber which owned the underlying receive. Any other
failure cleared `BroadcastState.recv_ready` while peers remained
blocked on its unreachable event.

Publish ordinary receive exceptions as terminal broadcast state.
The owner keeps the original failure while peers drain retained
values and then raise `BroadcastReceiveError` from that cause. Also
wake peers on process-control exits without retaining them as state.

Document the public owner/peer contract and cover current, late and
control-flow subscribers with deterministic bounded regressions.

Prompt-IO: ai/prompt-io/opencode/20260812T030608Z_1095e7f7_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi 5afd834732 Fix `BroadcastState.statistics()` queue counts
`BroadcastState.subs` stores each receiver's next unread deque
index, but `.statistics()` exposed that index as a queue length. A
caught-up receiver looked correct by accident while every queued
count was one short.

Convert cursors to retained, receivable counts and clamp lagged
receivers to the current queue length. Also avoid deprecated
`trio.Event` truthiness when reporting waiter counts.

Cover caught-up, queued, lagged and real-event states using actual
broadcast sends and receives.

Prompt-IO: ai/prompt-io/opencode/20260812T012324Z_06c4af17_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi 206b0354c5 Fix `BroadcastReceiver` lag counts
`BroadcastReceiver.receive_nowait()` treated `seq` as a deque index
but subtracted `BroadcastState.maxlen` without counting the first
invalid index. A one-slot queue thus claimed it dropped zero values
after its subscriber missed one.

Include that first displaced value in the count. Preserve the
existing Tokio-style reset to the oldest retained item.

Also, cover exact loss reporting and recovery for one- and
three-slot retention windows.

Prompt-IO: ai/prompt-io/opencode/20260811T233833Z_7cbd64ee_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-27 21:18:00 -04:00
Gud Boi 24e55e918e Document canonical `/run-tests` deployment
Link Tractor's project reference to the immutable canonical skill
and its repository, then show the exact hybrid deployment command.

Also clarify the Prompt-IO record's commit boundaries and PR #510
review direction.

Review: PR #510 (copilot-pull-request-reviewer)
https://github.com/goodboy/tractor/pull/510#pullrequestreview-5046306236

Prompt-IO: ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-27 19:47:31 -04:00
Gud Boi c9cfce441a Harden `run-tests` signal and reaper guidance
Prevent unsafe harness guidance from sending SIGUSR1 before
`stackscope` installs its handler or using shared-memory scans on
unsupported platforms.

Deats,
- verify `stackscope` in the pytest env before signaling.
- target pytest by default and require a separately verified handler
  before signaling subactors.
- keep `tractor-reap --shm*` guidance to Linux and FreeBSD.
- pin staged Prompt-IO provenance to `eb3c99c9` and record the
  PR #510 review direction.

Prompt-IO: ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-27 18:52:16 -04:00
Gud Boi eb3c99c9fd Correct `run-tests` harness guidance
The migration reference overstated cleanup safety and omitted
current environment, platform, debugger and timeout behavior.

- document runtime env overrides and the actual CI matrix
- distinguish interactive debugger capture from hang diagnosis
- describe Trio-aware and SIGALRM timeout guards separately
- expose reaper platform limits and dry-run descendant expansion
- restore exact change-to-test mappings and randomized-port wording
- drop unrelated Taken and broad command ignores from the migration

Prompt-IO: ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-27 18:34:48 -04:00
Gud Boi 0664731372 Retry macOS tests with `pytest-rerunfailures`
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`))
2026-08-27 13:43:06 -04:00
Gud Boi d5e66dffaa Restore high-fanout startup cancellation stress
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`))
2026-08-26 17:00:44 -04:00
Gud Boi 09e78ad087 Bind named `to_actor.run()` inputs with partials
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`))
2026-08-25 19:20:43 -04:00
Gud Boi dd91195377 Doc the #477 migration outcome + one-shot-acm sketch
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
2026-08-25 19:19:28 -04:00
Gud Boi 5420b13482 Doc ria-reap hang fix + paused reaper re-scope
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`))
2026-08-25 17:57:12 -04:00
Gud Boi d6da42984f Doc step-B2 handler-merge + prompt-io
Split from the step-B2 code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: add a "Step-B2
  outcome" section — the dead-outer-`raise` trace, why the
  merge is behavior-preserving, and the gate results.
- `ai/prompt-io/claude/20260702T222544Z_9201a2ed_*`: NLNet
  provenance (log + unedited raw) for the step-B2 work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-25 17:57:12 -04:00
Gud Boi 601d92eb4a Doc step-B outcome + prompt-io
Split from the step-B code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: add a "Step-B
  outcome" section — the empty-nursery collapse, why it's
  behavior-preserving, the deliberate handler-merge deferral,
  and the targeted-gate result.
- `ai/prompt-io/claude/20260702T172233Z_5cd190c5_*`: NLNet
  provenance (log + unedited raw) for the step-B work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-25 17:57:12 -04:00
Gud Boi 5f49544cf3 Add `_ria_nursery` removal plan + step-A prompt-io
Split from the step-A code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: agent-verified
  machinery map + 3-step (A/B/C) design + probe history
  (reap-relocation deadlock -> sequencing fix -> bound
  tighten) + risk register for the `._ria_nursery` excision.
- `ai/prompt-io/claude/20260702T165806Z_a34aaf98_*`: NLNet
  provenance (log + unedited raw) for the step-A work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-25 17:57:12 -04:00
Gud Boi 9373e9434d Inline `functools.Placeholder` lookup
Partial normalization assigned the optional Python 3.14 placeholder
sentinel separately from its only conditional consumer.

Bind the sentinel with a walrus expression directly in the guard while
retaining the `getattr()` fallback for older Python versions.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260825T021319Z_ce430fca_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 22:19:21 -04:00
Gud Boi ce430fca64 Clarify provisional child registration
Child monitors register before their IPC handshake so cancellation
owns every started process, but the bare `None` portal arg obscured
that `Portal(chan)` replaces the provisional entry after connection.

Document that transition and name all `_register_child()` args in both
spawn backends. Replace the MP test's positional-only lambda with a
signature-accurate fake which asserts the provisional portal state.

Caught-during: review remediation
Found-via: `/run-tests` test_mp_late_registration_never_starts_process

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260825T015742Z_e42ecb55_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 22:11:15 -04:00
Gud Boi e42ecb559d Use `Aid` keys for child reap state
The fresh reap-coordination maps still used legacy `.uid` tuples even
though process monitors and channels carry complete `Aid` identities.
This extended the legacy key format into new private state.

Key both reap maps by `Aid` and derive `.uid` only when accessing the
existing `_children` map. UUID-based `Aid` hashing lets the subactor
and decoded channel identities resolve the same synchronization state.

Update registration tests to exercise the full identity keys.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260824T233957Z_5327b25e_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 21:56:00 -04:00
Gud Boi 5327b25e1b Factor `child_in_debug()` state sampling
`_try_cancel_then_kill()` repeated the same child/tree debugger
predicate before and after its cancel-RPC checkpoint. Inline
duplication obscured that lock state must be sampled at both points.

Factor the predicate into a local `child_in_debug()` sampler. Use it
for initial hard-kill protection and re-run it after the await before
debugger waiting, preserving dynamic lock-state behavior. Keep it
local since one input is supervisor-owned nursery configuration.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260824T225356Z_2f86dd1a_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 19:38:22 -04:00
Gud Boi 2f86dd1a33 Assert paired `ActorNursery` reap state
`._mark_child_reaped()` previously discarded the reap-request event
without checking that its completion-event peer existed. A one-sided
entry would silently lose process-reap synchronization.

Capture both pops and assert paired presence while allowing the valid
both-absent startup-failure path. Keep an unset request valid because
backend cancellation can reap immediately after registration.

Extend graceful and failed-cancel-ack runtime tests to require all
child and reap mappings empty before `to_actor.run()` returns.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260824T223614Z_88d538e3_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 18:50:59 -04:00
Gud Boi 88d538e3a6 Share `Context.cancel()` deadline with frame sends
A parent-side ctx cancel timeout previously bounded only the
remote `_cancel_task` ack. Complete-frame `send_all()` shielding
could hold request publication forever when a peer stopped reading.

Compute one absolute deadline and pass it through `._run_from_ns()`
so transport publication and the ack wait consume the same timeout
budget. Add a mock-clock regression which stalls the private RPC
under a nested shield and proves the transaction returns on time.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

Prompt-IO: ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-24 18:34:19 -04:00
Gud Boi 5c4859860a Close late `ActorNursery` registration race
A child could pass the early `.start_actor()` guard, miss the
`.cancel()` child snapshot and register afterward. Its monitor
inherited a reap request without runtime cancellation and could wait
forever.

- publish child/reap events before sampling `_cancel_called`
- make MP abort before `proc.start()` when cancellation won
- kill a Trio child opened after cancellation won registration
- reject starts begun after nursery cancellation is already visible
- add deterministic registration and MP no-start regressions
- drop the touched Trio backend's stale `get_runtime_vars` import

Prompt-IO: ai/prompt-io/opencode/20260821T040803Z_3c1bbe73_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:35 -04:00
Gud Boi 23e26b5b32 Bound `Portal.cancel_actor()` frame sends
A cancel RPC could stall forever in complete-frame transport
shielding before the peer received it, bypassing the outer ack
timeout and blocking graceful supervision.

- thread one absolute deadline from `Portal.cancel_actor()` through
  the private `Start` publication path
- force-close a partial-frame stream before releasing its send lock
- keep ordinary sends unbounded and preserve pending cancellation
- document the current `Start -> StartAck -> CancelAck` exchange and
  link the dedicated `Cancel` msg follow-up in #506
- cover partial publication and the shared send/ack timeout budget

Prompt-IO: ai/prompt-io/opencode/20260821T023537Z_ae6f2ac3_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:35 -04:00
Gud Boi 086962ca36 Preserve cancellation over shielded `send_all()` errors
The frame-publication shield added in `88a23449` checks for
pending cancellation only after a successful write. If actor
teardown closes the stream first, `ClosedResourceError` escaped
as `TransportClosed` and could defeat the caller's cancel scope.

Check for pending cancellation on the transport-error path before
normalizing the close. Genuine stream errors retain their existing
translation when no cancellation is active.

The cancellation-first path can swap which nested debugger
intermediary renders as the immediate source vs. relay. Keep
assertions over both actor levels while accepting either valid role.

Prompt-IO: ai/prompt-io/opencode/20260820T143845Z_559fd0f1_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00
Gud Boi 1029b81dfc Fix macOS CI `skipif` condition
The Darwin-only debugger skip in `49fc92b0` passed the raw
`CI=true` env string to `skipif`, so `pytest` evaluated `true`
as Python source and failed at setup instead of skipping the
issue #320 node.

Cast `_ci_env` through `bool()` so the marker always receives
a boolean while retaining Linux coverage.

Prompt-IO: ai/prompt-io/opencode/20260820T135125Z_9f99043b_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00
Gud Boi 40d2d9942e Showcase `to_actor.run()` across docs
The rendered guides and executable examples still taught the legacy
`ActorNursery.run_in_actor()` result-portal model even though #481
adds its blocking, linked-context replacement.

Deats,
- migrate one-shots to direct results through `to_actor.run()`
- preserve named target inputs with `functools.partial()`
- use daemon actors where reciprocal dialogs need longer lifetimes
- link the new API from core, asyncio and clustering references
- retain only explicit legacy/removal notes

Prompt-IO: ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00