Compare commits

..

15 Commits

Author SHA1 Message Date
Gud Boi d9a6e2e9b4 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-17 17:25:20 -04:00
Gud Boi 13588d0216 Peel `wg` maddrs w/ `py-multiaddr`'s own tunnel API
`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`))
2026-08-17 17:25:07 -04:00
Gud Boi ee17ed9f6e 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-14 09:51:36 -04:00
Gud Boi 7d6e79551e Fix silently-corrupt keys in `parse_wg_maddr()`
`_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`))
2026-08-14 09:50:48 -04:00
Gud Boi cc85f17f5f Pin `multiaddr` to the merged `wg` codec rev
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`))
2026-08-14 09:38:58 -04:00
Gud Boi 0c96f92396 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-12 20:36:03 -04:00
Gud Boi 27c34aebb6 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-12 20:08:15 -04:00
Gud Boi bf974c9870 Add a `wg`-tunnelled 2-host example set
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`))
2026-08-12 20:08:15 -04:00
Gud Boi 41d08d04a6 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-12 20:08:15 -04:00
Gud Boi 208876cc3b 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-12 20:08:15 -04:00
Gud Boi f60bdc44c7 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-12 20:08:15 -04:00
Gud Boi 7806b60fff 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-12 20:08:15 -04:00
Gud Boi 7fab27de29 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-12 20:08:15 -04:00
Gud Boi f7e3fdea1a 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-12 20:08:15 -04:00
Gud Boi 402e74b97a 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-12 20:08:15 -04:00
178 changed files with 1908 additions and 10053 deletions

View File

@ -91,10 +91,6 @@ jobs:
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}' name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
timeout-minutes: 16 timeout-minutes: 16
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# Windows support is nascent: its full test suite remains
# informational, while setup and the `import tractor` smoke below
# are hard signals. Promote the test step to required once the
# suite is green.
strategy: strategy:
fail-fast: false fail-fast: false
@ -102,7 +98,6 @@ jobs:
os: [ os: [
ubuntu-latest, ubuntu-latest,
macos-latest, macos-latest,
windows-latest,
] ]
python-version: [ python-version: [
'3.13', '3.13',
@ -123,10 +118,10 @@ jobs:
'tcp', 'tcp',
'uds', 'uds',
] ]
# https://github.com/orgs/community/discussions/26253#discussioncomment-3250989
exclude: exclude:
# UDS is POSIX-only; Windows has no `AF_UNIX` so the # don't do UDS run on macOS (for now)
# backend is intentionally unavailable there. - os: macos-latest
- os: windows-latest
tpt_proto: 'uds' tpt_proto: 'uds'
steps: steps:
@ -155,18 +150,7 @@ jobs:
- name: List deps tree - name: List deps tree
run: uv tree run: uv tree
# hard signal for the Windows import-safety fix: `import
# tractor` must succeed everywhere, and `HAS_UDS` reflects
# platform capability (False on Windows, True on POSIX).
- name: 'Smoke: import tractor'
run: uv run python -c "import sys; import tractor; from tractor.ipc._uds import HAS_UDS; assert sys.platform != 'win32' or not HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)"
- name: Run tests - name: Run tests
# Actor/PTY scheduling on macOS can fail a different
# timing-sensitive node between otherwise-green runs. Retry
# only that matrix leg; deterministic failures still fail
# after the final attempt.
continue-on-error: ${{ matrix.os == 'windows-latest' }}
run: > run: >
uv run uv run
pytest pytest
@ -175,8 +159,6 @@ jobs:
--spawn-backend=${{ matrix.spawn_backend }} --spawn-backend=${{ matrix.spawn_backend }}
--tpt-proto=${{ matrix.tpt_proto }} --tpt-proto=${{ matrix.tpt_proto }}
--capture=fd --capture=fd
--reruns=${{ matrix.os == 'macos-latest' && 2 || 0 }}
--reruns-delay=1
# XXX legacy NOTE XXX # XXX legacy NOTE XXX
# #

View File

@ -1,463 +0,0 @@
# `_ria_nursery` removal plan (issue #477 follow-up)
Goal: drop the secondary "run-in-actor" spawn nursery (and
friends) from `ActorNursery`/spawn internals, now that
`tractor.to_actor.run()` delivers one-shot semantics purely on
the daemon-spawn + portal primitives.
## Verified machinery map (2026-07-02, wkt @ a34aaf98)
The entire mechanism is 4 files:
- `runtime/_supervise.py`
- `ActorNursery.__init__(.., ria_nursery, ..)` stores
`._ria_nursery` (:202, :238); sole read is
`run_in_actor()` passing `nursery=self._ria_nursery`
(:442) into `start_actor()`'s `nursery:
trio.Nursery|None` escape-hatch param (:305, :367).
- `._cancel_after_result_on_exit: set` (:244) marks ria
portals (:457).
- `_open_and_supervise_one_cancels_all_nursery()` nests
`da_nursery` (:609) around `ria_nursery` (:622); the
`finally:` at the ria->da boundary (:747-766) raises
collected `errors` (single exc or BEG).
- `runtime/_portal.py`
- `._expect_result_ctx` (:112) set by `_submit_for_result()`
(:142, sole caller `run_in_actor()`); consumed by
`wait_for_result()` (:167) + deprecated `result()` (:220).
The `None` branch (:184-196) returns the `NoResult`
sentinel (`_exceptions.py:1164`).
- `spawn/_spawn.py`
- `exhaust_portal()` (:129): awaits
`portal.wait_for_result()`, CATCHES+RETURNS any exc
(never raises).
- `cancel_on_completion()` (:177): `exhaust_portal()` ->
on exc-result stash `errors[uid] = result` (:203) ->
ALWAYS `portal.cancel_actor()` (:218).
- `spawn/_trio.py` (:195-222) + `spawn/_mp.py` (:187-213),
identical shape: after shielded
`await an._join_procs.wait()`, open a per-child local
nursery; IFF `portal in an._cancel_after_result_on_exit`
start `cancel_on_completion` alongside `soft_kill()`; when
`soft_kill` returns first, `nursery.cancel_scope.cancel()`
reaps the result-waiter.
## The load-bearing semantic (already-deferred errors)
Remote ria-child errors NEVER raise into `ria_nursery`:
1. reaper tasks only START after `_join_procs.set()` (block
exit or the inner error handler),
2. `exhaust_portal` swallows the exc into a return value,
3. `cancel_on_completion` stashes it in `errors` + cancels
that child,
4. the ria->da `finally:` re-raises collected `errors` (and
`an.cancel()`s any daemon stragglers).
So mid-block there is NO error propagation from ria children
(unless user code explicitly `await portal.wait_for_result()`s)
— the two-nursery nesting only sequences "reap ria results
BEFORE blocking on daemon join". A single-nursery impl only
needs to preserve that sequencing, not any ASAP-cancel
behavior.
## Target design
### step A: single-nursery `run_in_actor()` (mechanical)
- `run_in_actor()` spawns via the DEFAULT (`_da_nursery`)
path — drop `nursery=self._ria_nursery`.
- rename `._cancel_after_result_on_exit` ->
`._ria_portals: dict[portal, Actor]` (need the subactor ref
for `cancel_on_completion`).
- move reaper start-up OUT of the backends into
`_open_and_supervise...`: immediately after EACH
`an._join_procs.set()` call-site (happy path :642, inner
error handler :661), start one
`cancel_on_completion(portal, subactor, errors)` task per
ria portal into `da_nursery`, then (happy path only)
`await` their completion BEFORE falling out of the
`try:`/`finally:` that raises `errors` — e.g. gather in a
dedicated inner `trio.open_nursery()` block replacing
today's `ria_nursery` join point.
- delete the membership branch + local reaper nursery from
`_trio.py`/`_mp.py` (keep the `soft_kill()` call; the
per-child local nursery collapses to just `soft_kill`).
- `_trio.py:310` `_children.pop()` etc. unchanged.
### step B: delete the plumbing
- `_open_and_supervise...`: drop the inner
`ria_nursery` + merge its `except BaseException` classify
logic into ONE handler on the (now single) nursery scope;
`ActorNursery.__init__` loses the `ria_nursery` param.
- `start_actor()` loses the `nursery:` escape-hatch param
(the :302-304 TODO).
- backends: no more `_cancel_after_result_on_exit` refs.
### step C: (separate PRs) deprecate + migrate + excise
- migrate in-repo `.run_in_actor()` usage to
`to_actor.run()`: tests 46 hits/9 files (test_cancellation
15, test_infected_asyncio 10, test_spawning 8, registrar 3,
adv_streaming 4, pubsub 2, rpc 1, runtime 1), examples 28
hits/13 files (debugging/* dominate), docs 20 hits/8 rst
files. NOTE: many sites also use deprecated
`Portal.result()`/`wait_for_result()` — these die with
`_expect_result_ctx`, so migration must land FIRST.
- add `DeprecationWarning` to `run_in_actor()` (+
`_submit_for_result`/`wait_for_result`).
- final excision: `run_in_actor()`, `_submit_for_result`,
`_expect_result_ctx`, `wait_for_result`/`result`,
`exhaust_portal`, `cancel_on_completion`, `NoResult`.
## Risk register
1. hard-killed ria child: today the backend-local
`nursery.cancel_scope.cancel()` discards a still-parked
reaper when the proc dies first; a da_nursery-hosted
reaper instead sees the transport break ->
`exhaust_portal` returns a `TransportClosed`-ish exc ->
NEW entry in `errors` that today gets discarded. Guard:
reap-gather block must cancel remaining reapers once all
ria procs are dead, or filter transport-death excs for
already-`cancel_called` children.
2. error-path ordering: inner handler today sets
`_join_procs` THEN `an.cancel()`; reapers race the
cancel-RPC. Keep that ordering when moving reaper spawn.
3. debugger interplay: `maybe_wait_for_debugger()` calls
(:654, :730) must stay BEFORE any reap/cancel issuance.
4. `errors` double-entry: local body error (:646) + child's
relayed exc (via reaper) can both land for the same
scenario -> BEG shape changes vs today? (today has the
same dual-write sites; keep behavior identical.)
5. mp backend parity: mirror every `_trio.py` edit in
`_mp.py` (identical block).
## Step-A first-probe findings (2026-07-02, WIP in tree)
Step A is IMPLEMENTED (uncommitted):
`run_in_actor()` spawns via da_nursery; new
`_supervise._reap_ria_portals()` helper; reap awaited after
happy-path `_join_procs.set()`; error-path runs reap
CONCURRENT with `an.cancel()` in the shielded block;
backends stripped of the membership branch + per-child
reaper nursery (+ dead imports).
Probe history (trio backend):
- `tests/test_to_actor.py` + `tests/test_spawning.py`:
20/20 PASS — incl. all `run_in_actor()` result
round-trips + `test_remote_error` (single erroring child,
body re-raise -> inner error path).
- FIRST attempt ran the error-path reap CONCURRENT with
`an.cancel()` (mimicking the old backend-side race):
`test_cancellation.py::test_multierror` (2 erroring ria
children, body re-raises one) DEADLOCKED. Root cause per
the sequencing fix below: reap + cancel must NOT race at
this layer (suspected `._children` pop-during-iteration
and/or double-cancel RPC wedge; not fully root-caused
since the fix removes the race wholesale).
- FIX (2nd attempt, current impl): error path SEQUENCES:
(1) snapshot ria `(portal, subactor)` pairs (backend
`finally`s pop `._children` as procs reap), (2)
`await an.cancel()`, (3) bounded reap over the snapshot.
Bound was first 3s -> blew the `fail_after` deadline in
`test_cancel_while_childs_child_in_sync_sleep` (hard-
killed grandchild never relays => reaper parks the full
bound). Tightened to 0.5s: anything collectable is
already queued in the local ctx (relayed BEFORE the
cancel); a parked reaper self-cleans (`trio.Cancelled`
results are never stashed).
- RESULT: `tests/test_cancellation.py` FULLY GREEN
(20 passed, 1 xfailed, 77s); full-suite gate run kicked
off same session (see final report/next session).
Remaining risk: on slow CI a relayed-but-undelivered error
racing the 0.5s bound could drop an `errors` entry
(BEG-shape flake); if observed, scale the bound via the
`cpu_perf_headroom()`-style approach or peek
`Portal._final_result_msg`/ctx queue state instead of
time-bounding.
## Step-B outcome (2026-07-02, done in tree)
Step A landed as `5cd190c5` (code) + `99310269` (docs).
Step B implemented on top (uncommitted):
- `._ria_nursery` is GONE — the inner
`async with (collapse_eg(), trio.open_nursery() as
ria_nursery)` layer in
`_open_and_supervise_one_cancels_all_nursery` is deleted;
`da_nursery` is now the single nursery for ALL subactors.
- `ActorNursery.__init__` drops the `ria_nursery` param +
the `self._ria_nursery` attr; `start_actor()` drops its
`nursery=` escape-hatch param (uses `self._da_nursery`
directly).
- `._cancel_after_result_on_exit` STAYS — it's the
ria-child discriminator for `_reap_ria_portals()`.
Deliberately NOT done (deferred to its own higher-risk PR,
flagged with a TODO at the outer `except`): merging the two
error handlers into one. Rationale — collapsing the empty
nursery is provably behavior-preserving (a zero-task
`trio.open_nursery()` only adds a checkpoint), whereas the
inner `except BaseException` (swallow-into-`errors`) and
outer `except (...)` (re-raise, safety-net for the inner
handler's own non-shielded awaits) have DIFFERENT
semantics; merging changes error/cancel propagation and
wants isolated review + its own gate. Both handlers are
kept, now nested directly under the single nursery.
Why the collapse is safe: post-step-A NOTHING spawns into
`ria_nursery` (its only reader, `run_in_actor`'s
`nursery=self._ria_nursery`, was removed in A; the stored
attr was never read again). So the layer was pure dead
weight.
Gate (trio backend, all 0-failure):
- targeted set (`test_cancellation test_spawning test_local
test_rpc test_to_actor`) = 49 passed, 1 xfailed.
- tail set (`test_reg_err_types remote_exc_relay
resource_cache ringbuf root_infect_asyncio root_runtime
runtime shm task_broadcasting trioisms trionics/`) = 63
passed, 1 skipped, 5 xfailed.
- full-suite head ~73% (subdirs + `test_2way`..`test_pubsub`)
= 303 passed before the known-flaky `test_dynamic_pub_sub`
TooSlowError stall (pre-existing; same hang in the step-A
full run). Suite ran slow this session (~13min vs 555s
cold, likely thermal from back-to-back runs), never
completing within an 800s bound — but split across the
above three runs EVERY module passed under step B.
## Step-B2 outcome (2026-07-02, done in tree)
Step B committed as `9201a2ed` (code) + `d2e812fb` (docs), then
branched to `drop_ria_nursery`. Step B2 (the deferred
handler-merge) implemented on top (uncommitted):
- the two nested handlers in
`_open_and_supervise_one_cancels_all_nursery` collapse to
ONE `except BaseException as _scope_err` + the existing
`finally`. The `outer_err`/`inner_err` locals go away.
Why it's safe (trace, not hope): the OLD inner handler records
`errors[actor.aid.uid]` as its FIRST statement (before any
await). So whenever an error path runs, `errors` is non-empty.
The OLD outer handler was only reachable via leakage from the
inner handler (it catches `BaseException`, so nothing from the
`yield` scope bypasses it) — and by then `errors` is already
populated, so the `finally`'s `raise` from `errors` ALWAYS
superseded the outer handler's own `raise`. i.e. the outer
`raise` was dead. The outer handler's other effects
(`_scope_error`, a 2nd debugger-wait, child-cancel) are
redundant with the merged handler + `finally`. So one handler
+ `finally` is observably equivalent.
Residual nuance (accepted): in the rare "`trio.Cancelled`
delivered during the non-shielded `maybe_wait_for_debugger`"
path, the merged form may leave `_cancel_called` False (cancel
happens after the wait), so `open_nursery`'s tb-hiding guard
(`not cancel_called and _scope_error`) can show a tb it
previously hid. More informative, not less; no test asserts on
it.
Gate (box ran ~2.7x slow this session, load-induced
`TooSlowError` flakiness on timing tests — NOT code; see
[[env_cpu_throttle_masquerades_as_regression]]):
- baseline (pre-B2 tip `9201a2ed`) full suite
(`-k 'not dynamic_pub_sub'`) = 300 passed + 1
`test_ext_types_over_ipc` `TooSlowError` that passes 6/6 in
isolation (4.89s).
- B2 error/cancel gate (`test_cancellation remote_exc_relay
inter_peer_cancellation advanced_faults oob_cancellation
to_actor spawning local rpc`) = 71 passed, 1 xfailed
(125s).
- B2 full-suite run: see `b2_full.log` (result appended on
completion). RECOMMEND a clean full-suite run on a
normal-speed box before this merges.
## Regression + fix: ria-reap hang (2026-07-02)
Human hit a full-suite hang on
`test_infected_asyncio.py::test_tractor_cancels_aio`. Bisected:
passes at pre-ria `a34aaf98` (0.59s), hangs at B2 `e617b498`
(90s+). Root-caused to the STEP-A reaper hoist (`5cd190c5`),
NOT B2 (`_reap_ria_portals` is byte-identical A->B2).
Bug: the test does `run_in_actor(asyncio_actor)` then a USER
`portal.cancel_actor()` and exits the block cleanly -> the
happy path's `await _reap_ria_portals()`, which waits UNBOUNDED
on `cancel_on_completion -> wait_for_result()`. The child was
cancelled out-of-band so no final result is relayed -> parked
forever. The OLD spawn-backend reaper was raced against
`soft_kill()` (per-child nursery `cancel_scope.cancel()` on
subproc death); the hoist dropped that race.
Fix: `_reap_ria_portals()` runs each `cancel_on_completion()`
in a local nursery alongside a `proc.poll()` death-watch that
cancels the parked reaper once the subproc exits — restoring
the old race, backend-agnostic (guarded by
`hasattr(proc, 'poll')` for a future `subint` handle).
Why POLL (`proc.poll()`) not the event-driven `wait_func`:
the mp waiter (`_spawn.proc_waiter`) does
`wait_readable(proc.sentinel)`, and `soft_kill()` is ALREADY
awaiting that same fd concurrently in the daemon nursery — a
2nd `wait_readable` on one fd raises `trio.BusyResourceError`.
(`trio.Process.wait()` IS multi-waiter-safe, but mp has no
async equivalent.) `proc.poll()` — the same liveness check
`soft_kill` itself falls back to — is the conflict-free common
denominator. Verified: poll-fix passes on BOTH trio and
mp_spawn.
Also added a per-test anti-hang guard: wrapped
`test_tractor_cancels_aio`'s `main()` in
`with trio.fail_after(9 * cpu_perf_headroom())` — the blessed
pattern (`pytest-timeout`'s global cap is intentionally off;
breaks trio under fork backends, see `pyproject` NOTE). So a
future recurrence FAILS FAST instead of hanging the suite.
(Several other tests in the file are still guardless —
`test_aio_simple_error`, `test_trio_error_cancels_intertask_chan`,
`test_aio_errors_and_channel_propagates_and_closes` — candidate
follow-up sweep.)
Lesson: the B2 focused gate OMITTED `test_infected_asyncio`
(and the full runs were clipped/slow), so the step-A hang
slipped through. Any future ria-touching change MUST gate
`test_infected_asyncio` explicitly.
Gate: `test_tractor_cancels_aio` green (trio 1.53s, mp 3.98s);
fix gate (`test_infected_asyncio test_cancellation test_to_actor
test_spawning`) = 74 passed, 3 xfailed, 0 failures.
## PAUSED (2026-07-02): re-assess the reaper's SCOPE
User's insight (compelling — likely the real root cause of
the hang, not just the missing proc-death race):
> the "hoisting" of 5cd190c5 was just not really done right
> — the hoist should have been into the `to_actor` scope,
> not `_supervise`.
The argument: `.run_in_actor()`'s result-waiting/reaping got
hoisted into `_supervise._reap_ria_portals` (nursery-machinery
scope), which has NO natural cancel-scope to bound a parked
`wait_for_result()` — hence the awkward proc-death race +
the poll-vs-`proc_waiter` dilemma. If the result-wait instead
lived in the `to_actor` one-shot scope
(`to_actor._invoke_in_subactor()`), it would sit right next to
the caller's `an` + a local `trio` task-nursery + cancel-scope
(the `trio.to_thread`-style model #477 actually wants) — so
bounding/cancelling the wait is trivial and the hang
dissolves from correct scoping rather than a bolt-on race.
Follow-on to re-evaluate on resume:
- should `_reap_ria_portals` exist AT ALL, or should
result-waiting move entirely into
`to_actor._invoke_in_subactor()`?
- reimplement legacy `run_in_actor()` on top of
`to_actor.run()` so `_reap_ria_portals` +
`_cancel_after_result_on_exit` can be DROPPED from
`_supervise` entirely (the true #477 simplification)?
- the poll-vs-event decision is MOOT under this re-scoping.
State at pause: `test_infected_asyncio` anti-hang guard
COMMITTED (`d1fb4a1a`, intentionally red w/o the fix — the
user's failing-test-first convention). The poll-based reap
fix in `_supervise.py` is UNCOMMITTED and likely SUPERSEDED
by the re-scoping — do NOT land it as-is.
## RESOLVED (2026-07-06): migrate everything, remove the API
The PAUSED re-assessment concluded decisively: rather than
re-scope `_reap_ria_portals` (or bolt any hack onto it), the
`run_in_actor()` API itself was REMOVED — its non-blocking
"result at teardown" semantic predates streaming and confused
more than it served. Every in-repo caller was migrated
per-file/-group (each its own commit, each gated):
- tests: `test_infected_asyncio` `test_runtime` `test_rpc`
`test_spawning` `test_pubsub` `test_registrar`
`test_cancellation` (3 groups) `test_advanced_streaming`.
- examples: 4 non-debugging + all 8 `debugging/` REPL scripts
(debugger suite byte-identical green, 28p/6s).
- docs: 8 rst pages + the `experimental/_pubsub` docstring.
Migration patterns (the `run_in_actor` shape -> successor):
- blocking result -> `to_actor.run(fn, an=an, ...)`
- fire-&-forget/forever -> bg `to_actor.run()` task in a local
`trio` task-nursery (or `start_actor`
+ bg `Portal.run()` when a portal
handle is needed)
- concurrent fan-out -> N bg `to_actor.run()` tasks / or
`gather_contexts([p.open_context(..)])`
- reap-all-error-collect -> the "collect don't cancel" pattern:
each one-shot catches + stashes its
`RemoteActorError`, group raised
after the task-nursery joins (see
`examples/debugging/multi_subactors.py`)
- mutual-rendezvous -> peers must OUTLIVE both dialogs:
`start_actor()` daemons + concurrent
`Portal.run()`s + explicit
`an.cancel()` (eager one-shot reap
races the slower peer's dial of the
winner's dead sockaddr; found via
`test_trynamic_trio` flake).
Semantic deltas (tests loosened accordingly):
- teardown-reap-all BEG-of-N is GONE: local task-nurseries are
cancel-on-first, raced siblings' `Cancelled`s are absorbed,
and the runtime's `collapse_eg()` unwraps every single-member
group at each actor boundary — a fully-raced nested tree
relays a bare (annotated) `RemoteActorError` chain.
- `test_multierror_fast_nursery`'s obsolete BEG-of-25 assertion
deleted; `test_concurrent_start_error_reaps_all` retains its
high-fan-out startup/cancel/reap stress under caller-scoped
semantics.
- `test_nested_multierrors` re-purposed separately as deep-tree
cancel-cascade stress w/ a race-tolerant shape walk.
Final excision (after zero callers remained): `run_in_actor()`,
`._cancel_after_result_on_exit`, `_reap_ria_portals()`,
`Portal._submit_for_result/._expect_result_ctx/
.wait_for_result()/.result()`, `exhaust_portal()`,
`cancel_on_completion()`, `NoResult` — net -402 lines. The
reap-hang class (unbounded `wait_for_result` in machinery
scope) dissolves structurally: the only result-wait left lives
in the caller's task inside its own cancel-scope; the
`d1fb4a1a` anti-hang guard test passes by construction. The
poll-vs-`proc_waiter` debate is moot as predicted.
## Follow-up sketch: `to_actor.open_one_shot()` (run-async parity)
If deferred-result parity is ever wanted, the design that needs
NO runtime coupling, NO returned `Portal` and NO cancel-relay
`trio.Event` machinery:
async with to_actor.open_one_shot(
fn, an=an, **kws,
) as one_shot:
... # concurrent caller work
val = await one_shot.wait() # optional; errors always
# propagate at scope exit
an `@acm` that opens a private task-nursery, `start_soon`s ONE
task running the existing blocking `run()` and stashes the
value in a slot + sets a done-`trio.Event` (a memo, not a
cancel relay). Cancellation = plain scope-cancel of the acm's
nursery (the parked `Portal.run()` unwinds via `Cancelled`, the
shielded `cancel_actor()` reap still runs); a child error
raises into the acm scope so an un-`wait()`ed one-shot can
never silently drop its error. i.e. the old reaper's job is
done by scoping, not machinery. ~40 lines, all in
`to_actor/_api.py`, zero `_supervise` involvement.
## Verification gate
- per-migration-commit module gates on `trio` (+ `mp_spawn`
spot-gates incl. `test_infected_asyncio` per the B2 lesson);
`tests/devx/test_debugger.py` for the REPL flows.
- full suite on `trio` + `mp_spawn` at branch tip + CI matrix
via draft PR #484.

View File

@ -1,79 +0,0 @@
---
model: claude-fable-5
service: claude
session: f6c84722-471a-4458-9a80-e453fea9029f
timestamp: 2026-07-02T15:42:55Z
git_ref: 65bf9df5
scope: code
substantive: true
raw_file: 20260702T154255Z_65bf9df5_prompt_io.raw.md
---
## Prompt
Driver prompt file `ai/prompt-io/prompts/issue_477.md`:
> attempt to resolve
> https://github.com/goodboy/tractor/issues/477
> do it with /open-wkt.
(plus a hard stop-for-human-review deadline of 12:50PM
EST the same day)
Issue #477 asks to factor `ActorNursery.run_in_actor()`
(and possibly `Portal.run()`) out of the nursery
internals into a new `tractor.to_actor` wrapper
subpackage of "higher level one shot" single-remote-task
APIs, adopting the `trio.to_thread`/`anyio.to_process`
parlance, so that error collection/propagation moves up
into the caller's local `trio` scope and the nursery's
spawn machinery can eventually drop the
`._ria_nursery` coupling.
## Response summary
First-cut `tractor.to_actor` subpkg delivering the
one-shot API composed purely from the existing
daemon-spawn + portal primitives (`start_actor()` +
`Portal.run()` + `Portal.cancel_actor()`), leaving the
legacy `.run_in_actor()` machinery untouched (formal
deprecation deferred until in-repo usage migrates):
- `to_actor.run(fn, **fn_kwargs) -> Any`: spawn a
subactor, schedule `fn` as its lone remote task, wait
on and return its result, ALWAYS reaping the subactor
(shield-safe `finally`). Remote errors raise in the
caller's task as boxed `RemoteActorError`s.
- placement variants: `portal=` reuses a running actor
(no spawn/reap), `an=` spawns from a caller-managed
actor-nursery, neither opens a call-scoped private
`open_nursery()` (implicitly booting the runtime,
configurable via `runtime_kwargs`).
- fail-fast validation before any spawn: non-streaming
async fn required; `portal=`/`an=` mutually
exclusive; `runtime_kwargs` rejected alongside any
placement opt.
- `run_in_actor()` TODO/docstring now cross-reference
the successor API.
## Files changed
- `tractor/to_actor/__init__.py` — new subpkg,
re-exports `run`
- `tractor/to_actor/_api.py``run()` +
`_invoke_in_subactor()` + `_validate_one_shot_fn()`
- `tractor/__init__.py` — top-level `to_actor`
re-export
- `tractor/runtime/_supervise.py` — comment/docstring
pointers from `run_in_actor()` to the successor
- `tests/test_to_actor.py` — 11-test suite covering
all placement variants, error relay, the concurrent
worker-pool-ish pattern and arg validation
- `examples/parallelism/to_actor_one_shots.py`
runnable demo (auto-collected by
`test_docs_examples.py`)
## Human edits
None yet — pending human review (work paused before the
12:50PM EST deadline per the driver prompt).

View File

@ -1,100 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-02T15:42:55Z
git_ref: 65bf9df5
diff_cmd: git diff main..wkt/to_actor_subpkg
---
# Raw AI output (diff-ref mode)
All generated code is committed on the
`wkt/to_actor_subpkg` branch; per diff-ref mode each
file's verbatim content is reachable via the pointers
below rather than duplicated here.
## Generated files
> `git diff main..wkt/to_actor_subpkg -- tractor/to_actor/__init__.py`
New subpackage init: module docstring establishing the
`trio.to_thread`/`anyio.to_process` "run it over there"
parlance for actors, plus the single public re-export
`run as run` from `._api`.
> `git diff main..wkt/to_actor_subpkg -- tractor/to_actor/_api.py`
The one-shot invocation impl, composed entirely from the
lower level daemon-spawn + portal primitives as
prescribed by issue #477:
- `_validate_one_shot_fn()`: the `Portal.run()`
non-streaming-async-fn constraint checked up-front,
before any subactor is spawned.
- `_invoke_in_subactor()`: `an.start_actor()` ->
`Portal.run()` -> always-reap via
`Portal.cancel_actor()` in a `finally` (the cancel
req's bounded wait is internally shielded so the reap
also runs under caller-scope cancellation).
- `run()`: the public API. Placement options:
`portal=` (reuse a running actor, no spawn/reap),
`an=` (spawn from a caller-managed nursery), or
neither (private `open_nursery()` scoped to the call,
implicitly booting the runtime when needed, tunable
via pass-through `runtime_kwargs`). Spawn opts mirror
`ActorNursery.start_actor()`; `**fn_kwargs` are
relayed to the remote task. Errors raise in the
caller's task as boxed `RemoteActorError`s.
`runtime_kwargs` alongside any placement opt is a
hard `ValueError`, never silently ignored.
> `git diff main..wkt/to_actor_subpkg -- tractor/__init__.py`
Top-level `from . import to_actor as to_actor`
re-export.
> `git diff main..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
Comment/docstring-only: the `run_in_actor()` deprecation
TODO now points at the implemented `.to_actor.run()`
successor (checkbox ticked) and the method docstring
gains a NOTE steering users to the new API; remaining
TODO items are the `DeprecationWarning` emission +
in-repo usage migration.
> `git diff main..wkt/to_actor_subpkg -- tests/test_to_actor.py`
11-test suite: private-nursery one-shot, implicit
runtime boot via `runtime_kwargs`, remote-error relay to
the caller's task (bare + caller-managed nursery),
caller-nursery spawn, portal reuse w/o implicit reap,
the concurrent worker-pool-ish pattern (local `trio`
nursery x shared `an`), and the four validation
rejections (sync fn, async-gen fn, `portal`+`an`
combo, `runtime_kwargs`+placement combo).
> `git diff main..wkt/to_actor_subpkg -- examples/parallelism/to_actor_one_shots.py`
Runnable example (auto-collected by
`test_docs_examples.py`): the fully-implicit one-shot
plus the concurrent worker-pool-ish prime-check pattern
against a shared caller-managed actor-nursery.
## Test runs (verbatim)
```
tests/test_to_actor.py .......... [100%]
============= 10 passed in 4.29s =============
```
Regression subset for touched modules
(`test_local.py test_rpc.py test_spawning.py
test_cancellation.py`):
```
38 passed, 1 xfailed, 24 warnings in 80.71s (0:01:20)
```
(warnings are pre-existing stdlib `os.fork()`
DeprecationWarnings from the mp spawn backends, not
introduced by this change)

View File

@ -1,77 +0,0 @@
---
model: claude-fable-5
service: claude
session: b8ec2ffe-1e47-4db5-8bc3-5bebc5feaaf6
timestamp: 2026-07-02T15:50:06Z
git_ref: 65bf9df5
scope: code
substantive: true
raw_file: 20260702T155006Z_65bf9df5_prompt_io.raw.md
---
## Prompt
From `ai/prompt-io/prompts/issue_473.md`:
> NOTE: you MUST pause this work at 12:50PM EST (BEFORE
> your weekly limit reset) for review by a human!
>
> resolve GH #473: https://github.com/goodboy/tractor/issues/474
> do it with /open-wkt.
(The prompt's issue-number/URL mismatch was resolved in
favor of #473 — file name, session name and prompt text
all say #473; the `/474` URL is a typo.)
GH #473: "Root-cause UDS-on-macOS —
`uds_transport_actor_tree` example fails on CI".
## Response summary
Worked all four #473 next-step checkboxes in wkt
`uds_macos_473`:
1. **stderr surfacing**: `test_docs_examples.py` now
raises with the full subproc stderr (+stdout) on ANY
non-zero exit. The old last-line-'Error' check could
never fire for `tractor` crashes since root-actor
stderr always ends with the strict-EG collapse note
`( ^^^ this exc was collapsed from a group ^^^ )`
proven against the real PR #460 macOS CI log (bare
`assert 1 == 0`, no traceback).
2. **root-cause (linux-provable layer)**: macOS-only
addr corruption in
`MsgpackUDSStream.get_stream_addrs()` — no
`SO_PASSCRED`/autobind on darwin means the accept
side's `getpeername()` is `''`, and the
`(str(), str())` arm took `peername` unconditionally →
`Path('')` garbage addrs on every accepted conn.
Proven + fixed via linux no-autobind simulation.
Possibly not the final macOS crasher (non-fatal on
linux-sim); the diagnostic patch guarantees the next
macOS CI run shows any remaining layer.
3. **CI matrix**: removed the `macos-latest`+`uds`
exclude.
4. **un-skip**: dropped the macOS+CI skip of the example.
Also: `start_listener()` bindspace mkdir hardened
(`parents=True, exist_ok=True`), example docstring
peer-pid mechanism corrected for macOS.
## Files changed
- `tests/test_docs_examples.py` — surface full stderr on
non-zero exit; remove macOS skip of the UDS example
- `tractor/ipc/_uds.py` — fix no-autobind
`get_stream_addrs()` arm; harden bindspace mkdir;
document autobind semantics
- `.github/workflows/ci.yml` — add macOS UDS leg (remove
matrix exclude)
- `examples/uds_transport_actor_tree.py` — platform-
correct peer-pid docs
## Human edits
None yet — pending human review (hard-stop review
checkpoint at 12:50PM EST per prompt); commit staging and
any edits are the human's.

View File

@ -1,101 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-02T15:50:06Z
git_ref: 65bf9df5
diff_cmd: git diff main..wkt/uds_macos_473
---
# Raw output — GH #473 UDS-on-macOS root-cause session
NOTE: code output is diff-referenced (not copied) per
prompt-io diff-ref mode; all generated code is on branch
`wkt/uds_macos_473` relative to `main` (65bf9df5).
## Diagnostic narrative (verbatim)
Root-cause work proceeded by linux-side simulation since no
macOS host is available:
1. Pulled the actual macOS CI failure log from PR #460 run
28343878933 (job 84115107992): the failure is a bare
`assert 1 == 0` on `proc.returncode` with NO stderr
surfaced — the harness only re-raised when the LAST
stderr line contained 'Error'.
2. Reproduced the stderr-swallow mechanism on linux: any
`tractor` root-actor crash ends stderr with the
strict-EG collapse note
`( ^^^ this exc was collapsed from a group ^^^ )` which
never matches 'Error' — so EVERY possible crash was
swallowed. (Verified by sabotaging the runtime dir via
an over-long `XDG_RUNTIME_DIR` → `OSError: AF_UNIX path
too long` → rc=1 + swallowed.)
3. Found + proved a macOS-only addr-corruption bug in
`MsgpackUDSStream.get_stream_addrs()`: the
`(str(), str())` match-arm unconditionally took
`peername`, but on no-autobind platforms (macOS lacks
linux's `SO_PASSCRED`-triggered autobind) the accept
side's `getpeername()` is `''``Path('')` garbage
laddr/raddr on EVERY accepted UDS conn. Simulated on
linux by nulling `SO_PASSCRED` (no autobind → same `''`
shape): pre-fix the example printed
`listener sock file: .`; post-fix it prints the real
registry sockpath. Non-fatal on linux-sim (rc=0), so
possibly not the final macOS crasher — the diagnostic
patch guarantees the next macOS CI run reveals any
remaining layer.
4. Falsified the missing-parent-dir theory:
`get_rt_dir()` already `mkdir(parents=True,
exist_ok=True)`s at import (and macOS TCP CI passes),
so `~/Library/Caches/TemporaryItems` absence cannot be
the crasher. Hardened `start_listener()`'s bindspace
mkdir anyway (custom `filedir` case + racing actors).
## Generated changes (diff pointers)
> `git diff main..wkt/uds_macos_473 -- tests/test_docs_examples.py`
- always raise with FULL subproc stderr (+stdout) on any
non-zero example exit; keep legacy last-line 'Error'
check for zero-rc cases; drop the macOS+CI skip of
`uds_transport_actor_tree.py` (GH #473 next-step).
> `git diff main..wkt/uds_macos_473 -- tractor/ipc/_uds.py`
- `get_stream_addrs()`: document the autobind semantics
(bytes = linux abstract-ns autobind artifact), add
no-autobind `(str, str)` arm picking the non-empty name
(`peername` connect-side, `sockname` accept-side) with
an empty-pair `ValueError` guard.
- `start_listener()`: `bs.mkdir(parents=True,
exist_ok=True)`.
> `git diff main..wkt/uds_macos_473 -- .github/workflows/ci.yml`
- remove the `macos-latest`+`uds` matrix exclude so
UDS-on-macOS is exercised by CI (GH #473 next-step).
> `git diff main..wkt/uds_macos_473 -- examples/uds_transport_actor_tree.py`
- docs nit: peer-pid mechanism is `SO_PEERCRED` on linux,
`LOCAL_PEERPID` on macOS.
## Verification (verbatim summary)
- macOS-shape sim (no autobind) + fix: example rc=0 with
correct listener sockpath.
- native linux post-fix: example rc=0, autobind arms
unchanged.
- sabotage smoke-test: diagnostic patch surfaces the full
EG traceback incl. collapse-note line.
- `pytest tests/ipc/ tests/test_2way.py tests/discovery/
--tpt-proto uds`: 61 passed, 1 `TooSlowError` flake
(test_simple_rpc) that passes solo in 2.2s — attributed
to a concurrent full-suite run (other session) loading
the host.
- full `tests/test_docs_examples.py` run pending at
raw-file write time (queued behind the concurrent
session's suite).

View File

@ -1,83 +0,0 @@
---
model: claude-fable-5
service: claude
session: b6b42e23-0454-4d9b-be1c-0a89d65aed1b
timestamp: 2026-07-02T15:56:26Z
git_ref: 65bf9df5
scope: code
substantive: true
raw_file: 20260702T155626Z_65bf9df5_prompt_io.raw.md
---
## Prompt
From `ai/prompt-io/prompts/issue_470.md`:
> attempt to resolve
> https://github.com/goodboy/tractor/issues/470
> do it with /open-wkt, ensure you /run-tests on all
> changes
(plus a hard pause-for-human-review deadline of
12:50PM EST.)
Issue #470 asks to trim the ~0.42s `import tractor`
cost — which dominates per-actor spawn latency on the
`trio` backend — by lazy-importing heavy/optional deps
(`pdbp`, `stackscope`, `multiaddr`, + audit of
`colorlog`/`bidict`/`wrapt`/`setproctitle`).
## Response summary
Profiling showed the issue's dep-list only accounted
for ~20ms; the dominant cost (~244ms) was
`log.get_logger()`'s `get_caller_mod()` calling
`inspect.stack()` at module level in ~39 modules —
each call walks every stack frame (deep during nested
imports) and scans `sys.modules` per frame via
`inspect.getmodule()`.
Changes, in impact order:
1. `get_caller_mod()` -> `sys._getframe()` +
`f_globals['__name__']` `sys.modules` lookup
(~240ms saved).
2. Issue's lazy-import checklist: `bidict`,
`multiaddr`, `colorlog`, `wrapt` moved to
`TYPE_CHECKING`/function-local imports;
`platformdirs` function-local; `asyncio` +
`.to_asyncio` deferred out of the `devx.debug` +
`spawn._entry` eager paths (~15ms saved).
3. PEP 562 `__getattr__` on `tractor/__init__.py`
preserving public `tractor.to_asyncio` attr access.
Results: `import tractor` 0.42s -> ~0.145s (~65%);
sequential `start_actor` latency 0.40-0.44s ->
~0.179s/actor. `pdbp` (needs `_repl.py` class-base
restructure) + `platformdirs` (needs
`UDSAddress.def_bindspace` protocol rework) documented
as follow-ups.
## Files changed
- `tractor/log.py``get_caller_mod()` perf fix +
lazy `colorlog`
- `tractor/__init__.py` — PEP 562 lazy `to_asyncio`
- `tractor/discovery/_addr.py``bidict` ->
`TYPE_CHECKING`
- `tractor/discovery/_multiaddr.py` — lazy `multiaddr`
- `tractor/ipc/_tcp.py`, `tractor/ipc/_uds.py`
`Multiaddr` -> `TYPE_CHECKING`
- `tractor/runtime/_state.py` — lazy `platformdirs`
- `tractor/devx/_frame_stack.py` — lazy `pdbp` +
`wrapt`
- `tractor/devx/debug/_trace.py`,
`tractor/devx/debug/_tty_lock.py` — lazy `asyncio` +
`.to_asyncio`
- `tractor/spawn/_entry.py` — lazy
`run_as_asyncio_guest`
## Human edits
None yet — pending user review at the 12:50PM EST
pause gate (test-suite results reported in-session).

View File

@ -1,122 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-02T15:56:26Z
git_ref: 65bf9df5
diff_cmd: git diff main..wkt/boot_latency_470
---
# Raw AI output — gh #470 `import tractor` latency trim
All generated code is committed on the
`wkt/boot_latency_470` branch; per diff-ref mode each
file's content is referenced via its diff instead of
copied verbatim.
## Profiling findings (verbatim analysis output)
Baseline: `import tractor` ~0.39-0.42s wall.
`python -X importtime` + `cProfile` traced the cost NOT
primarily to third-party deps (the issue's hypothesis)
but to `tractor/log.py:get_logger()` calling
`get_caller_mod()` -> `inspect.stack()` at module level
in ~39 tractor modules:
- `inspect.stack()` builds `FrameInfo` (incl. src-file
and line-context resolution) for EVERY frame on the
stack; during nested imports the stack is dozens of
importlib frames deep.
- each `FrameInfo` resolution calls
`inspect.getmodule()` which scans all of
`sys.modules` per frame (1.4M `ismodule()` calls in
one profiled import).
- aggregate: ~244ms of tractor-own module "self" time
vs ~20ms for ALL the issue-listed third-party deps
(`pdbp` ~10ms, `bidict` ~4.5ms, `multiaddr` ~3.5ms,
`wrapt`/`colorlog` ~1ms each); `trio` itself is
~70-100ms and unavoidable.
## Generated changes
> `git diff main..wkt/boot_latency_470 -- tractor/log.py`
`get_caller_mod()` rewritten from `inspect.stack()` +
`inspect.getmodule()` to `sys._getframe(frames_up)` +
`frame.f_globals['__name__']` -> `sys.modules` lookup
(O(1) vs O(stack x sys.modules)). Unused `inspect`
imports dropped; `FrameType` imported from `types`.
Also `colorlog` lazy-imported inside
`get_console_log()`.
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_addr.py`
`bidict` import moved under `TYPE_CHECKING`
(annotation-only use; `_address_types` is a plain dict
literal).
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_multiaddr.py`
`from __future__ import annotations` added; `multiaddr`
import moved under `TYPE_CHECKING` + function-local
imports in `mk_maddr()`/`parse_maddr()`.
> `git diff main..wkt/boot_latency_470 -- tractor/ipc/_tcp.py tractor/ipc/_uds.py`
`Multiaddr` imports moved under `TYPE_CHECKING`
(annotation-only in both transports).
> `git diff main..wkt/boot_latency_470 -- tractor/runtime/_state.py`
`platformdirs` lazy-imported inside `get_rt_dir()`
(NOTE: still imported eagerly via
`UDSAddress.def_bindspace` class-var eval; see
follow-ups).
> `git diff main..wkt/boot_latency_470 -- tractor/devx/_frame_stack.py`
`pdbp` + `wrapt` lazy-imported inside
`hide_runtime_frames()` / `api_frame()` respectively.
> `git diff main..wkt/boot_latency_470 -- tractor/devx/debug/_trace.py tractor/devx/debug/_tty_lock.py`
`asyncio` moved to `TYPE_CHECKING` + call-site local
imports (`asyncio.current_task()` sites);
`tractor.to_asyncio.run_trio_task_in_future` imports
moved into the infected-aio runtime branches.
> `git diff main..wkt/boot_latency_470 -- tractor/spawn/_entry.py`
`run_as_asyncio_guest` import moved into the
`infect_asyncio=True` branches of `_mp_main()` /
`_trio_main()`.
> `git diff main..wkt/boot_latency_470 -- tractor/__init__.py`
PEP 562 module `__getattr__` added so
`tractor.to_asyncio` attr-access still works (required
by `tests/test_child_manages_service_nursery.py` and
any downstream user) while keeping `asyncio` off the
eager import path.
## Measured results (verbatim)
- `import tractor`: 0.39-0.42s -> ~0.145s (~65% cut)
- `start_actor` spawn+boot+reg+cancel: ~0.40-0.44s ->
~0.179s/actor (n=5 sequential, warm parent)
- post-change eager-module check: only `pdbp` +
`platformdirs` of the issue's list remain eager.
## Known follow-ups (not implemented, deadline-bound)
- `pdbp` (~10ms): still eager via
`devx/debug/_repl.py` class bases
(`class PdbREPL(pdbp.Pdb)`) + `_tty_lock.py`
module-level `@pdbp.hideframe`; needs `_repl`
restructure + PEP 562 in `devx.debug.__init__`.
- `platformdirs` (~1.5ms): eager via
`UDSAddress.def_bindspace: ClassVar = get_rt_dir()`
class-body call; needs `Address`-protocol rework of
`def_bindspace` to a lazy accessor.
- `stackscope` + `setproctitle`: already lazy/absent —
no change needed.

View File

@ -1,78 +0,0 @@
---
model: claude-fable-5
service: claude
session: f6c84722-471a-4458-9a80-e453fea9029f
timestamp: 2026-07-02T16:58:06Z
git_ref: a34aaf98
scope: code
substantive: true
raw_file: 20260702T165806Z_a34aaf98_prompt_io.raw.md
---
## Prompt
Follow-up round in the same session as the
`tractor.to_actor` landing (see
`20260702T154255Z_65bf9df5_prompt_io.md`). After
committing that work the user green-lit the deferred
items:
> go go go on this with what time you have left, in
> particular see if you can get the _ria_nursery
> removal going!
then extended the deadline twice to iterate on the
discovered hang:
> continue on this up until a 12:58:30 deadline
and finally chose "Commit step A now" from the
next-steps prompt.
## Response summary
Step A of the `._ria_nursery` removal (issue #477): hoist
`.run_in_actor()` result-reaping out of the spawn
backends into the `ActorNursery` machinery so ria
children spawn via the default daemon nursery,
- new `_supervise._reap_ria_portals()` runs one
`_spawn.cancel_on_completion()` task per ria child
AFTER `._join_procs` is set; happy path awaits it
right after `._join_procs.set()`.
- error path SEQUENCES: snapshot ria
`(portal, subactor)` pairs -> `await an.cancel()` ->
0.5s-bounded reap. Two failed intermediates informed
this: a concurrent reap+cancel DEADLOCKED
`test_multierror`; a 3s bound blew
`test_cancel_while_childs_child_in_sync_sleep`'s
`fail_after` deadline.
- backends (`spawn/_trio.py`, `spawn/_mp.py`) lose the
`._cancel_after_result_on_exit` membership branch,
per-child reaper nursery + dead imports.
- design/probe-history doc:
`ai/conc-anal/ria_nursery_removal_plan.md` (from an
agent-verified machinery map).
Verification: `test_cancellation.py` fully green
(20 passed, 1 xfailed) incl. the previously-hung
`test_multierror`; `test_to_actor`+`test_spawning`
20/20; bounded full-suite gate SIGINT'd ~30s early at
303 passed / 0 failures (user opted to commit on that
signal, deferring the unbounded re-run to step-B
verification).
## Files changed
- `tractor/runtime/_supervise.py``_reap_ria_portals()`
+ two call-sites; `run_in_actor()` off the ria nursery
- `tractor/spawn/_trio.py` — reaper branch + import drop
- `tractor/spawn/_mp.py` — same as `_trio.py`
- `ai/conc-anal/ria_nursery_removal_plan.md` — plan +
probe history
## Human edits
None yet — committed via the drafted
`.claude/git_commit_msg_ria_step_a.md` (user-driven
`git commit --edit`).

View File

@ -1,55 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-02T16:58:06Z
git_ref: a34aaf98
diff_cmd: git diff a34aaf98..wkt/to_actor_subpkg
---
# Raw AI output (diff-ref mode)
Step-A code is committed on `wkt/to_actor_subpkg`
directly after `a34aaf98`; per diff-ref mode the verbatim
content is reachable via the pointers below.
## Generated files
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
New `_reap_ria_portals(an, errors, ria_children=None)`
helper (one `_spawn.cancel_on_completion()` task per ria
child under `collapse_eg()` + a local nursery);
`run_in_actor()` drops `nursery=self._ria_nursery`; happy
path awaits the reap right after `._join_procs.set()`;
inner error handler snapshots ria pairs, runs
`await an.cancel()` then a `move_on_after(0.5)`-bounded
reap over the snapshot.
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/spawn/_trio.py`
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/spawn/_mp.py`
Both backends: the post-`_join_procs` block collapses to
a bare `soft_kill()` (membership branch, per-child reaper
nursery, reaper-cancel logging and the now-unused
`cancel_on_completion` imports all removed).
> `git diff a34aaf98..wkt/to_actor_subpkg -- ai/conc-anal/ria_nursery_removal_plan.md`
Agent-verified machinery map, 3-step design (A/B/C),
probe history (deadlock -> sequencing fix -> bound
tightening) and risk register.
## Test runs (verbatim)
```
tests/test_cancellation.py: 20 passed, 1 xfailed in 77.28s
tests/test_to_actor.py + tests/test_spawning.py: 20 passed
full-suite (SIGINT at 540s bound, ~30s short):
303 passed, 9 skipped, 1 xfailed, 2 xpassed in 539.42s
```
Failed intermediates (not in final diff): concurrent
reap+`an.cancel()` deadlocked `test_multierror`; a 3s
reap bound blew
`test_cancel_while_childs_child_in_sync_sleep`'s
`fail_after` deadline.

View File

@ -1,60 +0,0 @@
---
model: claude-opus-4-8
service: claude
session: c7860302-6a33-4d92-821c-0807d68be464
timestamp: 2026-07-02T17:22:33Z
git_ref: 5cd190c5
scope: code
substantive: true
raw_file: 20260702T172233Z_5cd190c5_prompt_io.raw.md
---
## Prompt
Branched session (`to_actor_subpkg_POST_WEEKLY_CUTOFF_2OPUS`,
now on Opus 4.8) continuing the `._ria_nursery` removal after
step A committed + pushed (`5cd190c5`/`99310269`):
> ok continue where your superior left off please
then, after the commit-split discussion:
> yup committed and pushed so take a look ma fren.
i.e. green-lighting step-B prep against the committed step-A
tree.
## Response summary
Step B of the `._ria_nursery` removal (issue #477): delete
the now-vestigial secondary nursery,
- collapse the inner `async with (collapse_eg(),
trio.open_nursery() as ria_nursery)` layer in
`_open_and_supervise_one_cancels_all_nursery``da_nursery`
becomes the single nursery for ALL subactors.
- `ActorNursery.__init__` drops the `ria_nursery` param + the
`self._ria_nursery` attr; `start_actor()` drops its
`nursery=` escape-hatch param.
- `._cancel_after_result_on_exit` kept (ria-child
discriminator for `_reap_ria_portals()`).
Verified behavior-preserving via a first-principles argument
(zero-task nursery = a bare checkpoint) + the targeted gate
(`test_cancellation test_spawning test_local test_rpc
test_to_actor` = 49 passed, 1 xfailed on trio). The two
error handlers were deliberately NOT merged — that changes
propagation semantics and is deferred to its own PR (TODO
left at the outer `except`).
## Files changed
- `tractor/runtime/_supervise.py` — collapse the ria nursery
layer + drop the ctor/`start_actor` params + refresh the
now-stale nursery comments
## Human edits
None yet — committed via the drafted
`.claude/git_commit_msg_ria_step_b.md` (user-driven
`git commit --edit`).

View File

@ -1,51 +0,0 @@
---
model: claude-opus-4-8
service: claude
timestamp: 2026-07-02T17:22:33Z
git_ref: 5cd190c5
diff_cmd: git diff 5cd190c5..wkt/to_actor_subpkg
---
# Raw AI output (diff-ref mode)
Step-B code lives on `wkt/to_actor_subpkg` after `5cd190c5`;
per diff-ref mode the verbatim content is reachable via the
pointer below.
## Generated files
> `git diff 5cd190c5..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
- `ActorNursery.__init__`: `ria_nursery` param removed;
`self._ria_nursery = ria_nursery` block deleted;
`_cancel_after_result_on_exit` comment refreshed.
- `start_actor()`: `nursery=` param removed; body uses
`self._da_nursery.start(...)` directly.
- `_open_and_supervise_one_cancels_all_nursery()`: the inner
`async with (collapse_eg(), trio.open_nursery() as
ria_nursery)` layer removed; `an = ActorNursery(actor,
da_nursery, errors)` constructed once under the single
`da_nursery`; the inner-try body de-indented one level;
both error handlers retained; the da-nursery lead comment
and the outer-`except` TODO refreshed to describe the
single-nursery reality + flag the (deferred) handler-merge.
> `git diff 5cd190c5..wkt/to_actor_subpkg -- ai/conc-anal/ria_nursery_removal_plan.md`
Added a "Step-B outcome" section (collapse rationale,
handler-merge deferral, safety argument, gate result).
## Test runs (verbatim)
```
targeted gate (trio):
tests/test_cancellation.py tests/test_spawning.py
tests/test_local.py tests/test_rpc.py tests/test_to_actor.py
-> 49 passed, 1 xfailed in 88.62s
signature checks:
ActorNursery.__init__ params: ['self', 'actor', 'da_nursery', 'errors']
start_actor has nursery param: False
full-suite gate: in progress (see stepB_full.log)
```

View File

@ -1,68 +0,0 @@
---
model: claude-opus-4-8
service: claude
session: c7860302-6a33-4d92-821c-0807d68be464
timestamp: 2026-07-02T22:25:44Z
git_ref: 9201a2ed
scope: code
substantive: true
raw_file: 20260702T222544Z_9201a2ed_prompt_io.raw.md
---
## Prompt
Continuing on branch `drop_ria_nursery` after step B
(`9201a2ed`). User green-lit the deferred error-handler merge:
> yes, i checked us out on a new branch for the ria-tn removal;
> work off the new wkt branch drop_ria_nursery.
and (re the migration) "nope, later we'll do this", plus:
> do ensure the test suite is green in the current state and
> those going forward ya?
and the standing reminder: no commits without a human ack.
## Response summary
Step B2 of the `._ria_nursery` removal (issue #477): merge the
two nested error handlers in
`_open_and_supervise_one_cancels_all_nursery` into one, now
that the 2ndary nursery (whose `__aexit__` the outer handler
originally guarded) is gone.
Trace-driven justification (see
`ai/conc-anal/ria_nursery_removal_plan.md` Step-B2 section):
the old inner handler records `errors[uid]` as its FIRST
action, so `errors` is always non-empty by the time control
could reach the outer handler — making the outer handler's
`raise` DEAD (always superseded by the `finally`'s
raise-from-`errors`). Collapsing to a single
`except BaseException as _scope_err` + the existing `finally`
is therefore behavior-preserving.
- removed the outer `try` + the
`except (Exception, BaseExceptionGroup, trio.Cancelled)`
handler; de-indented the inner handler one level.
- dropped the now-unused `outer_err`/`inner_err` locals
(single `_scope_err`).
- net ~30 lines lighter.
Gated hard: baseline (pre-B2 tip) = 300 passed + 1
load-induced `TooSlowError` flake (passes 6/6 in isolation);
B2 error/cancel gate (`test_cancellation remote_exc_relay
inter_peer_cancellation advanced_faults oob_cancellation
to_actor spawning local rpc`) = 71 passed, 1 xfailed; B2
full-suite run following.
## Files changed
- `tractor/runtime/_supervise.py` — collapse the two handlers
into one; drop `outer_err`/`inner_err`
## Human edits
None yet — committed via the drafted
`.claude/git_commit_msg_ria_b2.md` (user-driven
`git commit --edit`).

View File

@ -1,55 +0,0 @@
---
model: claude-opus-4-8
service: claude
timestamp: 2026-07-02T22:25:44Z
git_ref: 9201a2ed
diff_cmd: git diff 9201a2ed..drop_ria_nursery
---
# Raw AI output (diff-ref mode)
Step-B2 code lives on `drop_ria_nursery` after `9201a2ed`; per
diff-ref mode the verbatim content is reachable via the pointer
below.
## Generated files
> `git diff 9201a2ed..drop_ria_nursery -- tractor/runtime/_supervise.py`
`_open_and_supervise_one_cancels_all_nursery`:
- removed the outer `try:` wrapper and the
`except (Exception, BaseExceptionGroup, trio.Cancelled) as
_outer_err:` safety-net handler.
- the former inner `except BaseException` is now THE handler,
renamed local `_inner_err` -> `_scope_err`, de-indented one
level; it sets `an._scope_error`, records `errors[uid]`,
waits on the debugger, `_join_procs.set()`, then a shielded
classify/log + snapshot-ria + `an.cancel()` + 0.5s-bounded
`_reap_ria_portals()`. No re-raise (the `finally` raises
from `errors`).
- `finally` block unchanged.
- dropped the `outer_err`/`inner_err` local decls at fn top.
(The diff is large — ~119+/149- — because de-indenting the
handler body one level rewrites every line in the block; the
logic delta is just "two handlers -> one".)
## Test runs (verbatim)
```
baseline (pre-B2, step-B tip 9201a2ed), full suite
(dynamic_pub_sub deselected):
1 failed, 300 passed, 9 skipped, 2 deselected, 1 xfailed,
2 xpassed in 1499.49s
-> the 1 failure = test_ext_types_over_ipc[...] trio.TooSlowError
(load-induced; passes 6/6 in isolation in 4.89s)
B2 error/cancel gate:
tests/test_cancellation test_remote_exc_relay
test_inter_peer_cancellation test_advanced_faults
test_oob_cancellation test_to_actor test_spawning test_local
test_rpc
-> 71 passed, 1 xfailed in 125.26s
B2 full-suite run: see b2_full.log
```

View File

@ -1,81 +0,0 @@
---
model: claude-fable-5
service: claude
session: 6db64ac6-6986-4505-9343-df4ee31e67db
timestamp: 2026-07-06T17:28:18Z
git_ref: ad42871e
scope: code
substantive: true
raw_file: 20260706T172818Z_ad42871e_prompt_io.raw.md
---
## Prompt
Resuming the `drop_ria_nursery` (wkt) branch after the human's
review pause at `ba4af0ad`, with the directive:
> we need to resolve the remaining outstandings ... to finish
> this endeavour we ideally,
> - finish the ria-tn hoist into the machinery in `to_actor` if
> possible.
> - avoid supporting the `.run_in_actor()` non-blocking API
> going forward other then if we can re-wrap `to_actor.run()`
> per prior discussion with opus4.8.
> - get the test harness completely green locally and in CI -
> AND ESPECIALLY without any hangs due to the "the old
> spawn-backend reaper raced `soft_kill()`'s scope-cancel"
> - update all docs to reflect these changes once we're happy.
plus mid-flight additions:
> make sure both all the test and example code uses the naming
> `an: ActorNursery` for variable names
> where we at in terms of a potential hackaround design for
> supporting a to_actor.run_async() ... without requiring the
> same malarky ... with a `trio.Event` relay ... i'm totes fine
> with rejigging the iface to NOT return a Portal as well.
## Response summary
Completed the entire #477 `run_in_actor` removal on
`drop_ria_nursery` (16 commits this session), gated per-commit:
- ported the remaining `test_cancellation` groups
(`some_cancels_all`, `nested_multierrors`+`spawn_and_error`,
SIGINT + sync-sleep) — one-shots become concurrent
`to_actor.run()` tasks in local task-nurseries; BEG-shape
assertions loosened for cancel-on-first + the runtime's
`collapse_eg()` single-member unwrap (a fully-raced nested
tree relays a bare annotated `RemoteActorError` chain).
- fixed a pre-existing `UnboundLocalError` (`timeout` `match`
had no default arm for non-trio/MTF backends).
- ported `test_dynamic_pub_sub`, 4 non-debugging examples, all
8 `debugging/` examples (debugger suite byte-identical,
28p/6s; `multi_subactors` introduces the "collect don't
cancel" reap-all replacement pattern), 8 docs pages + the
`experimental/_pubsub` docstring.
- EXCISED the API + cluster: `run_in_actor`,
`_reap_ria_portals`, `_cancel_after_result_on_exit`,
`Portal._submit_for_result/_expect_result_ctx/
wait_for_result/result`, `exhaust_portal`,
`cancel_on_completion`, `NoResult` — net -402 lines. The
reap-hang class dissolves structurally (result-waits now only
in caller task-scope).
- found + fixed a real migration race: mutual-rendezvous peers
(`test_trynamic_trio`, `a_trynamic_first_scene.py`) flaked
because an eagerly-reaped one-shot dies while its peer still
dials the registry-resolved (dead) sockaddr — such peers now
pin lifetimes via `start_actor()` + concurrent `Portal.run()`
+ explicit `an.cancel()`.
- `an: ActorNursery` naming sweep across tests/examples (±82
lines, scoped renames, prose untouched).
- parked a `to_actor.open_one_shot()` design sketch (acm +
private task-nursery over blocking `run()`; done-Event as
memo not cancel-relay; no Portal) in the plan doc.
## Files changed
See commits `d01a2123..ad42871e` on `drop_ria_nursery`
(tests, examples, docs, `tractor/{runtime,spawn,to_actor,msg}`
+ `_exceptions/_context/experimental`).

View File

@ -1,39 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-06T17:28:18Z
git_ref: ad42871e
diff_cmd: git diff ba4af0ad..ad42871e
---
# Raw AI output (diff-ref mode)
This session's output spans the 16 migration/excision commits
`d01a2123..ad42871e` on `drop_ria_nursery`; per diff-ref mode
the verbatim content is reachable via the pointer below.
## Generated files
> `git diff ba4af0ad..ad42871e`
Commit-wise (each `Gate:`-footed msg documents its own module
gate):
- `d01a2123` port `test_some_cancels_all`
- `697c6152` fix unbound `timeout` (non-trio/MTF `match` arm)
- `fa8799d5` port `test_nested_multierrors`
- `f11754ce` port SIGINT + sync-sleep cancel tests
- `cb6202e3` port `test_dynamic_pub_sub`
- `d8af5f12` port non-debugging examples
- `a3057cb2` port debugging examples (+ `test_debugger`
nested-nurseries final-shape expectations)
- `d6bed7c4` port docs (8 rst pages)
- `07e1669e` fix stale `@pub` docstring example
- `2a59cefb` REMOVE `run_in_actor()` + the ria reap cluster
(net -402 lines)
- `a297a32a` fix mutual-rendezvous premature-reap race
- `ad42871e` `an: ActorNursery` naming sweep
Plan/design record updated in
`ai/conc-anal/ria_nursery_removal_plan.md` (RESOLVED section +
the `to_actor.open_one_shot()` follow-up sketch).

View File

@ -1,39 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: pr475-review-fixes-20260817
timestamp: 2026-08-17T23:18:25Z
git_ref: 359fe75c
scope: code
substantive: true
raw_file: 20260817T231825Z_359fe75c_prompt_io.raw.md
---
## Prompt
Continue the `/code-review-changes` pass for PR #475 in its isolated
worktree. Address the seven accepted manual-review findings in
`tractor/ipc/_types.py` and `tractor/ipc/_uds.py`, preserve the existing
Windows capability behavior, verify the result, and prepare the work for
human-controlled commit and review-reply steps. Do not publish replies,
stage, commit, or push without the required explicit authorization.
## Response summary
Restored project quote, docstring, multiline-expression, and
`match/case` conventions while retaining the Windows-safe UDS guard.
Removed unnecessary structural and comment churn, then verified the
focused transport, discovery, and lazy-import paths plus the missing
`AF_UNIX` behavior.
## Files changed
- `tractor/ipc/_types.py` - restore project style and guarded
socket-family dispatch.
- `tractor/ipc/_uds.py` - format the UDS capability gate
consistently.
## Human edits
None - the generated patch remains uncommitted and awaits human
review.

View File

@ -1,45 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-17T23:18:25Z
git_ref: 359fe75c
diff_cmd: git diff HEAD~1..HEAD
---
Applied the seven accepted manual-review fixes for PR #475 while
preserving the Windows transport capability behavior.
> `git diff HEAD~1..HEAD -- tractor/ipc/_types.py`
The generated changes restore the project's single-quote docstring and
string conventions, remove the unnecessary helper divider, simplify the
transport-registry comments, and restore `match/case` socket-family
dispatch. The UDS case retains a `HAS_UDS` guard that short-circuits
before `socket.AF_UNIX` is evaluated on unsupported hosts. Nearby error
messages are wrapped without changing their content.
> `git diff HEAD~1..HEAD -- tractor/ipc/_uds.py`
The generated change reformats the `HAS_UDS` conjunction according to
the project's multiline boolean-expression convention and simplifies
the adjacent capability comment.
Verification:
`/home/goodboy/repos/tractor/py313/bin/pytest -q tests/test_lazy_imports.py tests/discovery tests/ipc/test_server.py`
Result: `66 passed, 2 xpassed in 60.62s`.
`ruff check --no-cache --output-format=json tractor/ipc/_types.py tractor/ipc/_uds.py`
Result: no findings.
`git diff --check`
Result: no whitespace errors.
An explicit missing-`AF_UNIX` probe set `HAS_UDS = False`, removed the
socket constant, and exercised an unsupported socket family. It raised
the expected `NotImplementedError` instead of `AttributeError`.
No review replies, commits, or pushes were published.

View File

@ -1,43 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: pr481-review-fixes-p1-20260818
timestamp: 2026-08-18T03:15:32Z
git_ref: 4151b956
scope: code
substantive: true
raw_file: 20260818T031532Z_4151b956_prompt_io.raw.md
---
## Prompt
Address the approved review findings on PR #481, but work
iteratively: implement and verify one finding at a time, prepare a
separate `/commit-plan` after each fix, and stop for the human commit
before starting the next finding. Begin with the P1 per-child
lifecycle issue. Also publish the already-approved review findings
against the reviewed PR head before editing.
## Response summary
Published the approved non-approving review at head `4151b956`, then
implemented only the P1 lifecycle fix. Owned one-shot actors now use
a child-specific cancellation and process-reap handshake, including
hard escalation for unacknowledged cancellation and deterministic
bookkeeping removal before `to_actor.run()` returns.
## Files changed
- `tractor/runtime/_supervise.py` - coordinate child-specific cancel
and reap.
- `tractor/spawn/_trio.py` - wait on the Trio child's reap request.
- `tractor/spawn/_mp.py` - wait on the multiprocessing child's reap
request.
- `tractor/spawn/_spawn.py` - publish monitor completion centrally.
- `tractor/to_actor/_api.py` - await owned-child process reaping.
- `tests/test_to_actor.py` - cover cleanup, escalation, and startup
ordering.
## Human edits
None - the generated P1 patch remains uncommitted for human review.

View File

@ -1,74 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T03:15:32Z
git_ref: 4151b956
diff_cmd: git diff HEAD~1..HEAD
---
Implemented only the P1 lifecycle finding from the approved PR #481
review, preserving the requested one-fix-at-a-time commit boundary.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
Added per-child reap request/completion events to `ActorNursery`, a
shielded child-specific cancel-and-reap operation, late-registration
latching for nursery teardown, and cancellation escalation that waits
for debugger release before using non-ignorable process termination.
The nursery-wide cancellation path snapshots child records before
checkpointing so concurrent one-shot cleanup cannot invalidate its
iteration.
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
Changed Trio child monitors to wait on their per-child reap requests.
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
Changed multiprocessing child monitors to wait on their per-child reap
requests.
> `git diff HEAD~1..HEAD -- tractor/spawn/_spawn.py`
Ensured every backend publishes child-reap completion after its process
monitor exits.
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py`
Changed owned one-shot cleanup to await child-specific process joining
and bookkeeping removal instead of treating the cancel RPC as reaping.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Added regressions for immediate caller-managed nursery cleanup, failed
cancel acknowledgement escalation, and child registration after a
latched nursery-wide teardown request.
Verification:
`pytest -q tests/test_to_actor.py tests/test_cancellation.py tests/test_spawning.py tests/discovery/test_multi_program.py`
Result: `46 passed, 1 xfailed, 3 xpassed`.
`pytest -q tests/test_to_actor.py --tpt-proto uds`
Result: `13 passed`.
`pytest -q tests/test_to_actor.py --spawn-backend mp_spawn --tpt-proto tcp`
Result: `13 passed`.
One broad verification run was mistakenly launched in parallel with
the UDS and `mp_spawn` actor suites. It timed out
`test_remote_error_from_caller_nursery`; the node passed immediately
in isolation and the complete broad selection then passed serially.
The failure was classified as concurrent test-session interference,
not accepted as a passing boundary result.
Ruff, Python compilation, and `git diff --check` passed for the changed
boundary. Ruff's existing `_trio.py` F401 finding was reproduced at the
unmodified PR head and excluded from attribution to this patch.
No source files were staged, committed, pushed, or used for review
replies. The previously approved top-level review was published before
the fix at reviewed head `4151b956`.

View File

@ -1,36 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-18T19:30:01Z
git_ref: bf06b4f8
scope: code
substantive: true
raw_file: 20260818T193001Z_bf06b4f8_prompt_io.raw.md
---
## Prompt
Fix cancellation during an indeterminate transport write without
allowing a second RPC msg to corrupt the stream. Keep the transport
fix separate from the higher-level `to_actor` remediation and explain
the length-prefixed protocol rather than using unexplained "framed
send" terminology.
## Response summary
Close a msgpack transport stream when cancellation interrupts its
length-prefixed `send_all()` operation. The stream can no longer be
safely reused because Trio cannot report how many bytes were written.
## Files changed
- `tractor/ipc/_transport.py` - close an interrupted send stream.
- `tests/ipc/test_each_tpt.py` - cover cancellation during the write.
## Human edits
The human required this transport edge-case fix to land as its own
behavioral commit with a detailed message. During staged review, the
human also rejected the unexplained "framed send" wording and asked
for terminology tied directly to the actual transport operation.

View File

@ -1,19 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T19:30:01Z
git_ref: bf06b4f8
diff_cmd: git diff HEAD~1..HEAD
---
Prospective review found that cancellation can interrupt
`MsgpackTransport.send()` after `send_all()` writes only part of its
length-prefixed msg. Sending a cancellation request afterward can
append another msg to the indeterminate stream and desynchronize the
peer decoder.
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py tests/ipc/test_each_tpt.py`
Close the stream under a cancellation shield when `send_all()` is
cancelled. Cover the behavior with a fake stream that checkpoints
inside the write and records forced closure.

View File

@ -1,37 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-18T19:30:02Z
git_ref: bf06b4f8
scope: code
substantive: true
raw_file: 20260818T193002Z_bf06b4f8_prompt_io.raw.md
---
## Prompt
Distill repeated `Actor._contexts.pop()` machinery into a wrapper like
the RPC-task registration helper so future teardown sites do not keep
reconstructing the context-registry key independently. Preserve the
existing lifecycle-specific cleanup behavior.
## Response summary
Add idempotent `Actor._drop_context()` registry removal keyed from the
context's own channel and CID. Use it for caller context teardown and
the strict callee-side RPC deregistration path.
## Files changed
- `tractor/runtime/_runtime.py` - own context-registry removal.
- `tractor/runtime/_rpc.py` - use the helper for callee teardown.
- `tractor/_context.py` - use the helper after caller teardown.
## Human edits
The human identified the repeated registry-pop code and requested a
central primitive analogous to `_register_rpc_task()`. The agent first
suggested an async helper that also closed receive channels; the final
design was narrowed to registry removal only so each lifecycle owner
retains its existing closure, debugger, shielding, and error policy.

View File

@ -1,18 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T19:30:02Z
git_ref: bf06b4f8
diff_cmd: git diff HEAD~1..HEAD
---
Repeated teardown sites reconstruct the `Actor._contexts` registry
key from a portal channel and context ID before popping it. Add an
idempotent actor-owned helper deriving the key from the context itself,
then route caller and callee context teardown through that helper.
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tractor/runtime/_rpc.py tractor/_context.py`
Keep receive-channel closure and cancellation shielding in each
lifecycle owner so the helper centralizes registry machinery without
changing their teardown ordering.

View File

@ -1,38 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-18T19:30:03Z
git_ref: bf06b4f8
scope: code
substantive: true
raw_file: 20260818T193003Z_bf06b4f8_prompt_io.raw.md
---
## Prompt
Cancel a remote task when its caller is cancelled after `Start`
publication but before startup acknowledgement. Keep cancellation
bounded, prevent its private `_cancel_task` RPC from recursively
cancelling itself and preserve public target kwargs unchanged.
## Response summary
Add private portal startup policy, use it for non-recursive context
cancellation and clean caller-side startup state under a shield.
## Files changed
- `tractor/runtime/_portal.py` - separate private startup policy.
- `tractor/_context.py` - disable recursion for cancellation RPCs.
- `tractor/runtime/_runtime.py` - clean cancelled task startup.
- `tests/test_context_stream_semantics.py` - control cancellation
between `Start` publication and acknowledgement.
## Human edits
The human required this cancellation behavior to remain a distinct
commit from general startup failures and from the public `to_actor`
API. The human also requested that its runtime comment describe the
actual length-prefixed transport guarantee and concrete `_cancel_task`
operation rather than referring to an unnamed wrapper.

View File

@ -1,20 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T19:30:03Z
git_ref: bf06b4f8
diff_cmd: git diff HEAD~1..HEAD
---
Cancellation while `Actor.start_remote_task()` waits for `StartAck`
can strand its caller-side context and leave the remote task running.
Make one bounded cleanup request, remove local startup state and close
its receive channel.
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tractor/runtime/_portal.py tractor/_context.py tests/test_context_stream_semantics.py`
Separate private startup-cancellation policy from public target kwargs
using `Portal._run_from_ns()`. Have `Context.cancel()` disable recursive
startup cancellation for its own `_cancel_task` RPC. Exercise
cancellation after `Start` publication and prove the caller-owned actor
remains reusable without leaked contexts.

View File

@ -1,36 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-18T19:30:04Z
git_ref: bf06b4f8
scope: code
substantive: true
raw_file: 20260818T193004Z_bf06b4f8_prompt_io.raw.md
---
## Prompt
Release caller-side context state for every remote-task startup failure,
not only local cancellation. Preserve the remote error, avoid unsafe
follow-up sends and prove pre-publication serialization failures leave
a reused portal healthy.
## Response summary
Extend remote-task startup cleanup across send, acknowledgement and
validation errors. Track completed publication, perform only safe
best-effort cancellation and deterministically remove local state.
## Files changed
- `tractor/runtime/_runtime.py` - clean every startup failure path.
- `tests/test_context_stream_semantics.py` - cover authorization and
serialization failures before context entry.
## Human edits
The human accepted the discovered edge-case fixes but required general
startup cleanup to land separately from cancellation cleanup, transport
integrity and the public API. This boundary preserves that behavioral
distinction and its dedicated commit-message rationale.

View File

@ -1,19 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T19:30:04Z
git_ref: bf06b4f8
diff_cmd: git diff HEAD~1..HEAD
---
`Actor.start_remote_task()` inserts a context before sending `Start`,
but startup errors other than cancellation escape without removing or
closing that caller state. Serialization errors, acknowledgement
timeouts, malformed acknowledgements and remote authorization errors
can therefore leak context-registry entries.
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tests/test_context_stream_semantics.py`
Cover the complete send, acknowledgement and validation phase with
exceptional cleanup. Attempt remote cancellation only when publication
is known complete or protocol-safe, and always release local state.

View File

@ -1,52 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-18T19:30:05Z
git_ref: bf06b4f8
scope: code
substantive: true
raw_file: 20260818T193005Z_bf06b4f8_prompt_io.raw.md
---
## Prompt
Replace abandoned `Portal.run()` one-shots with a static linked-context
endpoint. Follow Trio positional-call semantics, use partials for target
keywords, preserve Python 3.14 Placeholder behavior, keep target lookup
behind the RPC allowlist and support private, nursery and portal
placement.
## Response summary
Use `Portal.open_context()` and `Context.wait_for_result()` for one-shot
tasks. Normalize every partial layer, validate signatures locally and
send target namespace/function components separately to the authorized
remote resolver. Retain the client-side function in its `NamespacePath`
so `to_tuple()` does not re-import it. Owned actors enable the declaring
`_api.__name__` directly; caller-owned portals opt in through the public
`to_actor.MODULE` alias.
## Files changed
- `tractor/to_actor/_api.py` - implement linked one-shot calls.
- `tractor/to_actor/__init__.py` - export `MODULE`.
- `tractor/msg/ptr.py` - retain refs created by `from_ref()`.
- `tests/test_to_actor.py` - cover the public API and authorization.
- `examples/parallelism/to_actor_one_shots.py` - use positional inputs.
## Human edits
The human rejected nested target-kwargs configuration and selected
Trio-style positional inputs plus `functools.partial()`. During staged
review the human required a Python 3.14 compatibility comment rather
than removing Placeholder support, requested separate namespace and
function inputs, preserved `_get_rpc_func(ns: str, funcname: str)`
authorization, renamed `RPC_MODULE` to `MODULE`, rejected global module
exposure and deferred speculative nursery/module-list helpers to the
`open_taskman()` design line. The human also required this public API
to land only after its lower-level safety dependencies. In final staged
review, the human required `_invoke_from_portal()` to use
`NamespacePath.to_tuple()` with the already-held function ref and
required internal actor setup to use `_api.__name__` directly, keeping
`to_actor.MODULE` solely as the public importer-facing alias.

View File

@ -1,24 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-18T19:30:05Z
git_ref: bf06b4f8
diff_cmd: git diff HEAD~1..HEAD
---
Implement `to_actor.run()` with Trio-style positional target arguments,
`functools.partial` keyword and Python 3.14 Placeholder binding, and a
static context endpoint that links remote results, errors and caller
cancellation.
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py tractor/to_actor/__init__.py`
Resolve target functions through `Actor._get_rpc_func()` so module
authorization remains authoritative. Automatically expose the helper
module for actors owned by `to_actor.run()` and document explicit
exposure for a caller-owned portal.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py examples/parallelism/to_actor_one_shots.py`
Cover placement modes, argument binding, nested partials, caller-linked
cancellation, remote errors and module authorization.

View File

@ -1,34 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
timestamp: 2026-08-19T02:07:57Z
git_ref: b38efed7
scope: code
substantive: true
raw_file: 20260819T020757Z_b38efed7_prompt_io.raw.md
---
## Prompt
Resolve the remaining P3 review finding before landing PR #481:
`runtime_kwargs={}` must not be silently accepted alongside either
`an=` or `portal=` merely because the dict is falsey. Keep this as its
own final review-remediation commit.
## Response summary
Treat any non-`None` `runtime_kwargs` value as provided when validating
placement. Cover both placement APIs with empty and configured dicts,
proving the error is raised locally before actor startup.
## Files changed
- `tractor/to_actor/_api.py` - validate option presence explicitly.
- `tests/test_to_actor.py` - cover four invalid option combinations.
## Human edits
No direct line edits. The human accepted the P3 finding, required it to
remain separate from the five P2 behavioral commits and prioritized it
before the final PR #484 integration rebase and PR #481 landing steps.

View File

@ -1,25 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T02:07:57Z
git_ref: b38efed7
diff_cmd: git diff HEAD~1..HEAD
---
Fix the final PR #481 review finding: `runtime_kwargs` is mutually
exclusive with both caller placement options whenever it is provided,
including an empty dict.
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py tests/test_to_actor.py`
Use an explicit `is not None` check rather than dict truthiness. Expand
the validation regression across `an=` and `portal=`, each with empty
and configured runtime kwargs, so every invalid combination fails
before actor runtime startup.
Verification:
- Trio/TCP: `23 passed`
- Trio/UDS: `23 passed`
- `mp_spawn`/TCP: `23 passed`
- Ruff and `git diff --check`: clean

View File

@ -1,56 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-19T18:46:40Z
git_ref: 481ba003
scope: code
substantive: true
raw_file: 20260819T184640Z_481ba003_prompt_io.raw.md
---
## Prompt
Rebase PR #484 onto final PR #481, migrate every affected one-shot call
to the new positional target API and continue through downstream tests,
examples and documentation review.
## Response summary
Converted stale target keyword calls to target partials so previously
named inputs remain explicit while placement/runtime controls stay
direct. Updated error expectations for local signature validation and
linked remote error propagation, then corrected docs which still
described the removed one-shot implementation. Linked spawning and
context lifecycle prose to the corresponding API methods and detailed
context guide.
## Files changed
- `docs/api/core.rst` - describe linked one-shot context execution.
- `docs/guide/rpc.rst` - update placement and target call semantics.
- `docs/guide/spawning.rst` - document positional target inputs.
- `examples/debugging/multi_nested_subactors_error_up_through_nurseries.py` - migrate nested actor target inputs.
- `examples/debugging/root_cancelled_but_child_is_in_tty_lock.py` - preserve named recursive target inputs with partials.
- `tests/test_advanced_streaming.py` - migrate streaming target inputs.
- `tests/test_cancellation.py` - migrate calls and tighten errors.
- `tests/test_infected_asyncio.py` - bind asyncio target options.
- `tests/test_rpc.py` - migrate RPC target argument binding.
- `tests/test_runtime.py` - preserve named runtime target inputs.
- `tests/test_spawning.py` - preserve named spawning target inputs.
## Human edits
The human selected the stack order and final PR #481 base, asked the
agent to continue after each diagnostic step and required a complete
commit plan after independently force-pushing the rebased history.
After reviewing the migration, the human required every formerly named
target input to remain visibly named through `functools.partial()`
rather than becoming positional. These were human-directed agent edits;
the human also required plain `start_actor()` and `open_context()`
references in the spawning and RPC guides to link to their API methods
and the detailed context guide, then clarified that `to_actor.run()`
already uses the full context API while `Portal.run()` should share
linked lifecycle machinery without necessarily delegating through
`Portal.open_context()` or adding a `Started` message. The human made
no direct source-line edits.

View File

@ -1,30 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T18:46:40Z
git_ref: 481ba003
diff_cmd: git diff HEAD~1..HEAD
---
Migrate PR #484's downstream one-shot calls to PR #481's final
`tractor.to_actor.run()` contract after the stack rebase.
> `git diff HEAD~1..HEAD -- docs examples tests`
Pass target arguments positionally and bind target keyword-only inputs
with `functools.partial()`. Keep placement and runtime controls as
direct `to_actor.run()` keywords. Update the invalid-target-argument
test to expect local signature binding before actor startup and require
direct `RemoteActorError` propagation from linked one-shots.
Update API and guide prose to describe positional target inputs,
linked `Portal.open_context()` execution and per-child reaping instead
of the removed `Portal.run()` and target-`**kwargs` conventions.
Verification:
- core and migrated runtime batches: `97 passed`
- discovery and related lifecycle batch: `33 passed, 1 skipped`
- changed executable examples: `9 passed`
- mapped debugger cases: `12 passed, 6 skipped`
- Ruff, compilation and `git diff --check`: clean

View File

@ -1,37 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-19T23:48:23Z
git_ref: 557065d8
scope: tests
substantive: true
raw_file: 20260819T234823Z_557065d8_prompt_io.raw.md
---
## Prompt
Investigate PR #481's red CI run, explain the missing T-800 and
debugger-output failures, and proceed with fixes in the PR #481
worktree.
## Response summary
Updated stale teardown assertions to match #481's direct hard-reap
path and observable process-lifetime invariants. Made nested debugger
checks consume the complete pexpect transcript rather than only the
last prompt latch.
## Files changed
- `tests/devx/test_debugger.py` - assert EOF/dead-process teardown and
accumulate nested debugger output across prompt boundaries.
- `tests/devx/test_tooling.py` - assert cancel-timeout hard-reap
escalation instead of the bypassed T-800 backend marker.
## Human edits
The human reported the still-red PR #481 CI, supplied a failing job URL,
required work in `/wkts/pr481_review_fixes` and directed the agent to
continue immediately. No direct source-line edits were made by the
human.

View File

@ -1,26 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T23:48:23Z
git_ref: 557065d8
diff_cmd: git diff HEAD~1..HEAD
---
Diagnose and fix the stale debugger and reaper assertions failing PR
#481's Unix CI jobs.
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py tests/devx/test_tooling.py`
Replace the old T-800 backend-log requirement with the new bounded
cancel-ack escalation evidence. Prove debugger teardown with EOF and a
dead child process instead of requiring optional `KeyboardInterrupt`
text. Accumulate all pexpect prompt chunks for nested error propagation
so expected tracebacks are not lost when `child.before` advances.
Verification:
- exact failed debugger/reaper nodes: `4 passed`
- debugger/tooling TCP: `39 passed, 6 skipped`
- debugger/tooling UDS: `39 passed, 6 skipped`
- full TCP suite: `478 passed, 9 skipped, 7 xfailed, 3 xpassed`
- full UDS rerun: `476 passed, 11 skipped, 8 xfailed, 2 xpassed`

View File

@ -1,43 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-19T23:48:24Z
git_ref: 557065d8
scope: code
substantive: true
raw_file: 20260819T234824Z_557065d8_prompt_io.raw.md
---
## Prompt
Investigate and fix PR #481's macOS TCP clustering and stream-overrun
failures without sacrificing IPC frame integrity or structured
concurrency.
## Response summary
Changed cancellation during `send_all()` from actor-wide stream closure
to shielded complete-frame publication followed by immediate pending
cancellation. Prevented failed overrun error shipment from promoting a
secondary transport closure over the context-local primary condition.
## Files changed
- `tractor/ipc/_transport.py` - complete in-flight frames before
delivering sender cancellation.
- `tractor/_context.py` - absorb transport closure while reporting an
overrun on an already-closing channel.
- `tests/ipc/test_each_tpt.py` - prove complete framing, cancellation
delivery and channel reuse.
- `tests/test_context_stream_semantics.py` - prove overrun reporting
tolerates a closed transport.
## Human edits
The human reported PR #481's red CI, asked for diagnosis and directed
the agent to proceed in the dedicated PR #481 worktree. During final
review, the human required preservation of the original far-end
cancellation rationale and fuller documentation of frame shielding,
shared-channel ownership and cancellation-delay tradeoffs. These were
human-directed agent edits; the human made no direct source-line edits.

View File

@ -1,31 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T23:48:24Z
git_ref: 557065d8
diff_cmd: git diff HEAD~1..HEAD
---
Fix the macOS TCP regressions where cancellation during a framed send
closed the actor-wide channel and replaced primary stream errors with
secondary `TransportClosed` failures.
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py tractor/_context.py tests/ipc/test_each_tpt.py tests/test_context_stream_semantics.py`
Shield complete frame publication, then deliver pending cancellation
immediately after leaving the shield. Preserve channel reuse instead of
closing the multiplexed socket from a context-local sender. Treat
`TransportClosed` while shipping `StreamOverrun` as failed delivery so
the secondary error can not crash the actor-wide RPC loop.
Add deterministic unit regressions for cancellation in the middle of a
frame and overrun reporting after transport closure.
Verification:
- transport/context unit regressions: `3 passed`
- exact TCP and UDS CI-node batches: `11 passed, 1 skipped`
- transport/context/clustering/RPC TCP: `88 passed`
- transport/context/clustering/RPC UDS: `86 passed, 2 skipped`
- full TCP suite: `478 passed, 9 skipped, 7 xfailed, 3 xpassed`
- full UDS rerun: `476 passed, 11 skipped, 8 xfailed, 2 xpassed`

View File

@ -1,32 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-20T02:30:04Z
git_ref: 88a23449
scope: tests
substantive: true
raw_file: 20260820T023004Z_88a23449_prompt_io.raw.md
---
## Prompt
Inspect the two failed macOS jobs in PR #481's new CI run and continue
toward a green landing candidate.
## Response summary
Confirmed both jobs fail only the known nested crash-REPL scenario from
issue #320, while Ubuntu TCP/UDS and Windows pass. Added a targeted
macOS-CI skip without reducing Linux coverage.
## Files changed
- `tests/devx/test_debugger.py` - skip the issue #320 nested
crash-REPL node on Darwin CI.
## Human edits
The human monitored the new CI run, reported both macOS jobs dead and
directed the agent to continue diagnosis. No direct source-line edits
were made by the human.

View File

@ -1,23 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T02:30:04Z
git_ref: 88a23449
diff_cmd: git diff HEAD~1..HEAD
---
Diagnose the remaining macOS PR #481 CI failures after the Linux
debugger and transport fixes passed.
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py`
Both macOS transports failed the same deeply nested crash-REPL test
already tracked by issue #320: TCP omitted one actor-specific traceback
record and UDS timed out waiting for a nested prompt. Apply an explicit
Darwin-CI skip to this one node while retaining Linux TCP/UDS coverage.
Verification:
- debugger/tooling TCP: `39 passed, 6 skipped`
- debugger/tooling UDS: `39 passed, 6 skipped`
- Ruff, compilation and `git diff --check`: clean

View File

@ -1,40 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-20T02:30:05Z
git_ref: 88a23449
scope: docs
substantive: true
raw_file: 20260820T023005Z_88a23449_prompt_io.raw.md
---
## Prompt
Audit all documentation and executable examples once more, replacing
prescriptive `run_in_actor()` usage with `to_actor.run()` or explicit
actor/context lifetime APIs before PR #481 lands.
## Response summary
Rewrote one-shot documentation around direct blocking result delivery,
linked context execution and per-call reaping. Migrated all runnable
examples, using daemon actors where reciprocal dialogs require longer
lifetimes. Added API/guide cross-links and retained only three explicit
legacy references.
## Files changed
- `docs/` - update API, quickstart and subsystem guides to showcase
`tractor.to_actor.run()` and link its underlying core APIs.
- `examples/` - migrate one-shot calls and preserve explicit daemon
lifetimes for reciprocal or long-lived actor dialogs.
## Human edits
The human requested a final docs pass covering every place that should
showcase `to_actor` over `.run_in_actor()`. Earlier review also required
named target arguments to remain visible through `functools.partial()`
and core API references to link to local guides/reference pages. These
were human-directed agent edits; the human made no direct source-line
edits.

View File

@ -1,27 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T02:30:05Z
git_ref: 88a23449
diff_cmd: git diff HEAD~1..HEAD
---
Perform a final rendered-documentation and executable-example pass so
PR #481 showcases `tractor.to_actor.run()` instead of the legacy
`ActorNursery.run_in_actor()` API.
> `git diff HEAD~1..HEAD -- docs examples`
Migrate one-shot guides and examples to direct result delivery through
`to_actor.run()`, preserving named target inputs with target partials.
Use daemon actors and concurrent portal calls where reciprocal actor
lifetimes require both peers to coexist. Add API and guide cross-links,
and retain only explicit legacy/removal notes.
Verification:
- executable docs examples: `23 passed`
- debugger/tooling TCP: `39 passed, 6 skipped`
- debugger/tooling UDS: `39 passed, 6 skipped`
- Ruff, compilation and `git diff --check`: clean
- local Sphinx build unavailable because Sphinx is not installed

View File

@ -1,35 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-20T13:51:25Z
git_ref: 9f99043b
scope: tests
substantive: true
raw_file: 20260820T135125Z_9f99043b_prompt_io.raw.md
---
## Prompt
Continue preparing PR #481 for landing after the prior test and
documentation commits were pushed. Follow CI and proceed with clear next
steps without merging or changing remote content unasked.
## Response summary
Followed CI through completion and found both macOS jobs failed because the
new `skipif` expression returned the `CI=true` environment string. Corrected
the condition to pass pytest a boolean before evaluating the marker. A
simulated Darwin-CI run now skips cleanly, and the sequential TCP and UDS
debugger/tooling suites each pass with 39 passed and 6 skipped.
## Files changed
- `tests/devx/test_debugger.py` - coerce the Darwin-CI skip condition to a
boolean.
## Human edits
The human pushed the preceding commits, directed the agent to continue, and
approved recording this test-only follow-up in Prompt-IO. No direct
source-line edits were made by the human.

View File

@ -1,22 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T13:51:25Z
git_ref: 9f99043b
diff_cmd: git diff HEAD~1..HEAD
---
Continue preparing PR #481 for landing after the test and documentation
commits were pushed. Follow the new CI run to completion and diagnose any
failures.
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py`
Both macOS jobs failed while evaluating the new `skipif` marker. The
expression returned the `CI=true` environment string instead of a boolean,
so pytest evaluated `true` as Python source and raised `NameError` during
test setup. Coerce `_ci_env` to `bool` so pytest receives a boolean marker
condition on Darwin CI.
Verification should exercise the condition with `CI=true` and a simulated
Darwin platform, then rerun the debugger/tooling TCP and UDS suites.

View File

@ -1,43 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-20T14:38:50Z
git_ref: 559fd0f1
scope: code
substantive: true
raw_file: 20260820T143845Z_559fd0f1_prompt_io.raw.md
---
## Prompt
Continue preparing PR #481 after the latest fix was pushed. Follow CI and
proceed with clear next steps toward a green landing candidate.
## Response summary
Traced the remaining macOS UDS failure to cancellation racing transport
teardown inside the shielded framed-send path. Preserve pending cancellation
over a transport error caused by concurrent teardown, and add a deterministic
regression for that ordering. A follow-up A/B run showed the corrected
cancellation precedence changes which nested debugger intermediary is
rendered as the immediate source versus relay, so retain coverage for both
actor levels without pinning those racy roles. The adjusted UDS node passes
three consecutive runs, and both debugger/tooling transport suites pass with
39 passed and 6 skipped.
## Files changed
- `tractor/ipc/_transport.py` - deliver pending cancellation before
translating a shielded send's transport error.
- `tests/ipc/test_each_tpt.py` - reproduce cancellation followed by local
stream closure during shielded frame publication.
- `tests/devx/test_debugger.py` - accept either valid source/relay role for
each nested intermediary while retaining the actor and error assertions.
## Human edits
The human pushed the preceding fix, ran the proposed verification plan, and
reported a repeated UDS debugger failure. That report prompted the A/B
comparison and role-insensitive assertion. No direct source-line edits were
made by the human.

View File

@ -1,26 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T14:38:50Z
git_ref: 559fd0f1
diff_cmd: git diff HEAD~1..HEAD
---
Continue preparing PR #481 after pushing the macOS debugger skip fix.
Follow the replacement CI run and address any remaining PR-specific
failure.
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py`
> `git diff HEAD~1..HEAD -- tests/ipc/test_each_tpt.py`
macOS UDS failed `test_reqresp_ontopof_streaming` when its two-second
`move_on_after()` scope cancelled during `stream.send('ping')`. Commit
`88a23449` shields framed `send_all()` and checks pending cancellation only
after a successful write. Concurrent transport teardown instead closed the
socket, causing `ClosedResourceError` to escape as `TransportClosed` before
the pending cancellation could be delivered.
Preserve structured cancellation precedence on the shielded send's
transport-error path, and add a deterministic regression that cancels the
sender before making the fake stream raise `ClosedResourceError`.

View File

@ -1,33 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 7b9c97c4-fff7-4ac4-97fb-35720453308e
timestamp: 2026-08-20T15:02:50Z
git_ref: pformat_caller_frame_render_guard
scope: code
substantive: true
raw_file: 20260820T150250Z_9afda1c6_prompt_io.raw.md
---
## Prompt
Fix both newly exposed send-side `MsgTypeError` formatting failures
and pin them with an end-to-end regression in PR #503.
## Response summary
Corrected codec-spec formatting and default error-message assembly so
`_mk_send_mte()` returns a printable error instead of raising another
formatter exception.
## Files changed
- `tractor/msg/_codec.py` - pass the codec to its supported formatter.
- `tractor/_exceptions.py` - assemble the default message as `str`.
- `tests/devx/test_pformat.py` - render the complete default error.
## Human edits
The human selected both one-line fixes and the single end-to-end test
as coherent additions to PR #503, while leaving broader formatter
cleanup out of scope.

View File

@ -1,25 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T15:02:50Z
git_ref: pformat_caller_frame_render_guard
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
After reviewing additional `tractor.devx.pformat` work suitable for
PR #503, the user approved fixing both send-side `MsgTypeError`
formatting failures and adding an end-to-end regression.
## Response
The generated code corrects the `MsgCodec.msg_spec_str` formatter
input, keeps `_mk_send_mte()`'s assembled default message a string,
and tests that the resulting `MsgTypeError` can be rendered:
> `git diff HEAD~1..HEAD -- tractor/msg/_codec.py tractor/_exceptions.py tests/devx/test_pformat.py`
These failures were hidden behind the original
`pformat_caller_frame()` keyword error addressed by the first two
commits on the branch.

View File

@ -1,65 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-21T02:35:37Z
git_ref: ae6f2ac3
scope: code
substantive: true
raw_file: 20260821T023537Z_ae6f2ac3_prompt_io.raw.md
---
## Prompt
Simplify bounded actor cancellation by passing an explicit absolute
deadline from `Portal.cancel_actor()` through `_run_from_ns()`,
`Actor.start_remote_task()`, and `Channel.send()` into
`MsgpackTransport.send()`. Avoid a `ContextVar`, watcher tasks, shared
status, coalescing, and waiter state. After tracing the current
`Start -> StartAck -> CancelAck` transaction, rename the local result to
`cancel_ack_received`, document its exact semantics, and link a focused
follow-up for a dedicated `Cancel -> CancelAck` protocol.
## Response summary
Threaded one absolute Trio deadline through the existing private
actor-cancel RPC path. The transport retains complete-frame shielding
for ordinary sends, while a cancel-control send that overruns its
deadline force-closes the potentially corrupted stream before releasing
the send lock. The outer actor-cancel scope uses the same deadline for
ack waiting and redelivers pending caller cancellation afterward.
Renamed the completion flag to `cancel_ack_received` and documented that
the current private call consumes `StartAck`, then receives a real
`CancelAck` after `Actor.cancel()` completes; this does not establish
that the OS process exited. Added a source TODO linking issue #506 for
the future first-class `Cancel -> CancelAck` transaction.
Focused transport and actor-cancel verification passed all four tests.
## Files changed
- `tractor/runtime/_portal.py` - own the absolute deadline, accurately
record ack receipt, and link the dedicated cancellation protocol.
- `tractor/runtime/_runtime.py` - forward the optional deadline for the
exact private `Start` publication.
- `tractor/ipc/_chan.py` - pass the operation-specific deadline to the
transport without changing ordinary sends.
- `tractor/ipc/_transport.py` - bound the shielded frame publication and
close a partial-frame stream before unlocking it.
- `tests/ipc/test_each_tpt.py` - cover deadline expiry after a partial
frame prefix reaches the stream.
- `tests/test_to_actor.py` - prove actor-cancel publication and ack
waiting share one absolute timeout budget.
## Human edits
The human rejected the initial watcher-task, shared `_SendStatus`, cancel
coalescing, and per-waiter design as unnecessary complexity. They also
rejected `ContextVar` propagation in favor of explicit functional
threading, selected a single absolute deadline for publication and ack
waiting, and required item 2 to remain separate from the item-3 child
reaping work. After reviewing the result, they requested the precise
`cancel_ack_received` name, a detailed protocol-trace comment, a focused
follow-up issue, and a linked source TODO. No direct source-line edits
were made by the human.

View File

@ -1,41 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-21T02:35:37Z
git_ref: ae6f2ac3
diff_cmd: git diff HEAD~1..HEAD
---
Replace the actor-cancel timeout watcher/status experiment with one
explicit absolute deadline threaded through the existing private call
path. Do not use a `ContextVar`, shared result state, waiter
coalescing, or polling tasks.
> `git diff HEAD~1..HEAD -- tractor/runtime/_portal.py`
`Portal.cancel_actor()` computes one absolute deadline and uses it for
both `Start` frame publication and the subsequent cancel-ack wait.
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py`
> `git diff HEAD~1..HEAD -- tractor/ipc/_chan.py`
The private RPC path forwards the operation-specific deadline. Lower
layers preserve the ordinary infinite-deadline call shape.
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py`
`MsgpackTransport.send()` applies the deadline inside its complete-frame
shield. If the deadline expires after partial publication, it closes
the unusable stream before releasing the send lock.
> `git diff HEAD~1..HEAD -- tests/ipc/test_each_tpt.py`
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Focused regressions prove a partial-frame timeout closes the stream and
that actor-cancel publication and acknowledgement share one budget.
The implementation removes the earlier `_SendStatus`, watcher task,
coalescing, shared cancel result, and per-waiter state. Four focused
transport and actor-cancel tests pass.

View File

@ -1,57 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-21T04:08:03Z
git_ref: 3c1bbe73
scope: code
substantive: true
raw_file: 20260821T040803Z_3c1bbe73_prompt_io.raw.md
---
## Prompt
Continue PR #481 review cleanup one item at a time. Compare the
remaining child-registration/reaping experiment against PR #484,
identify the next valid behavior, and generate an exact commit plan for
that boundary without committing automatically.
## Response summary
Separated late-child admission from the broader bounded-reap rewrite.
Added one synchronous `ActorNursery._register_child()` publication step
that stores the child and its reap events before sampling nursery
cancellation. This closes both scheduler orderings around
`ActorNursery.cancel()`'s child snapshot.
The MP backend registers immediately before synchronous process startup
and refuses to start when cancellation already owns the child. The Trio
backend registers immediately after `open_process()` and kills that
already-created process when registration observes cancellation. An
early `start_actor()` guard rejects calls begun after cancellation is
already visible.
Deterministic tests cover the nursery registration ordering and the MP
no-start invariant. Comparison with PR #484 confirmed that its retained
generic nursery/backends do not close this race.
## Files changed
- `tractor/runtime/_supervise.py` - atomically publish child ownership
and reject actor starts after nursery cancellation.
- `tractor/spawn/_mp.py` - register before synchronous process startup
and abort a cancellation-owned child.
- `tractor/spawn/_trio.py` - register immediately after process creation,
kill a cancellation-owned child, and remove its stale unused import.
- `tests/test_to_actor.py` - cover late registration and MP startup
suppression.
## Human edits
The human required review extras to be handled one item and one
behavioral commit at a time, with each item compared against PR #484
before acceptance. That direction split this late-registration fix from
the original broad experiment's bounded post-ack reaping,
`ActorNursery.cancel()` hard-reap rewrite, and debugger/error behavior.
The human accepted the narrower late-registration boundary by requesting
its commit plan. No direct source-line edits were made by the human.

View File

@ -1,48 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-21T04:08:03Z
git_ref: 3c1bbe73
diff_cmd: git diff HEAD~1..HEAD
---
Compare the remaining child-registration and reaping experiment with
PR #484, then identify the next review item without changing code.
The next item is the late-child admission race. A spawn can pass
`ActorNursery.start_actor()`'s early cancellation check, then be absent
from `ActorNursery.cancel()`'s child snapshot and register afterward.
The existing reap-request latch releases its monitor but does not send
runtime cancellation, so the monitor can wait forever for a still-live
process.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
`ActorNursery._register_child()` publishes the child, installs its reap
events, and samples `ActorNursery._cancel_called` without a checkpoint.
The two scheduler orderings are then complete: registration first puts
the child in the cancel snapshot, while cancellation first makes the
backend abort the late registration.
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
The multiprocessing backend registers immediately before `proc.start()`
and refuses to start a process already owned by nursery cancellation.
There is no Trio checkpoint between registration and process startup.
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
The Trio backend registers immediately after `open_process()` and kills
the newly opened process if cancellation won the registration race. Its
stale unused `get_runtime_vars` import is removed so the touched module
remains lint-clean.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Deterministic regressions prove late registration observes cancellation
and that the MP backend never starts a process after cancellation owns
its registration.
PR #484 retains the affected generic nursery and spawn-backend paths and
does not close this race. Keep this fix in PR #481 as its own commit;
review bounded post-`CancelAck` reaping separately.

View File

@ -1,37 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-addr-unpacking
timestamp: 2026-08-21T05:20:52Z
git_ref: 3690e43a
scope: config
substantive: true
raw_file: 20260821T052052Z_3690e43a_prompt_io.raw.md
---
## Prompt
The human asked for a main-first patch using an off-the-shelf pytest
plugin to cope with tractor's changing macOS CI flakes without mixing
that mitigation into PR #505.
## Response summary
Added `pytest-rerunfailures` to tractor's testing dependencies and
configured the GitHub Actions matrix to retry failures only on macOS.
Linux and Windows remain strict first-attempt runs, while persistent
macOS failures still fail after two visible reruns.
## Files changed
- `.github/workflows/ci.yml` - macOS-only pytest rerun budget.
- `pyproject.toml` - testing plugin dependency and rationale.
- `uv.lock` - resolved `pytest-rerunfailures` package metadata.
## Human edits
The human selected a main-first mitigation after PR #505 failed two
different macOS tests on consecutive runs and required the change to
remain an incremental patch with its own commit plan. The agent
implemented and verified that direction; no direct manual source
edits were observed.

View File

@ -1,25 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-21T05:20:52Z
git_ref: 3690e43a
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - retry flaky macOS CI tests
The human requested an off-the-shelf pytest plugin patch suitable
for landing directly on tractor `main` after PR #505's macOS job
failed two different timing-sensitive tests on consecutive runs.
> `git diff HEAD~1..HEAD -- .github/workflows/ci.yml pyproject.toml uv.lock`
Added the pytest-dev-maintained `pytest-rerunfailures` plugin and
gave only the macOS matrix leg two reruns with a one-second delay.
Linux and Windows receive a zero retry budget; deterministic macOS
failures still fail after the final attempt and reruns remain visible
in pytest output.
The lockfile is current, actionlint passed, all 471 tests collected,
and the four tests covering both observed PR #505 failure areas
passed with the rerun plugin enabled.

View File

@ -1,37 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-24T22:20:33Z
git_ref: ce38cb6f
scope: code
substantive: true
raw_file: 20260824T222033Z_ce38cb6f_prompt_io.raw.md
---
## Prompt
Continue the PR #481 review after triage. The human explicitly accepted
the proposed merge-blocking `Context.cancel()` deadline update with
"keep" and required the work to remain limited to that review item.
## Response summary
Update `Context.cancel()` so one absolute deadline bounds both shielded
cancel-request publication and acknowledgement waiting. Add a focused
mocked-clock regression for the blocked-publication failure mode and run
the narrow cancellation tests.
## Files changed
- `tractor/_context.py` - forward the cancel transaction's absolute
deadline to frame publication.
- `tests/test_to_actor.py` - prove blocked context-cancel publication is
bounded by the shared deadline.
## Human edits
The human retained ownership of review scope and explicitly selected
"keep" for this item after receiving keep/defer/drop options. The human
required no unrelated cancellation changes and did not directly edit
source lines.

View File

@ -1,26 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-24T22:20:33Z
git_ref: ce38cb6f
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 review update for `Context.cancel()`.
Use one absolute deadline for both cancellation-request frame
publication and acknowledgement waiting, without broadening the change
to unrelated cancellation behavior.
> `git diff HEAD~1..HEAD -- tractor/_context.py`
`Context.cancel()` computes one absolute cancellation deadline, uses it
for the outer bounded wait, and forwards it through
`Portal._run_from_ns()` to shielded frame publication.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
A deterministic mocked-clock regression arranges a shielded blocked
publication and proves that `Context.cancel()` forwards the same deadline
which bounds the complete cancel transaction.
Run the focused cancellation deadline regressions after the edit.

View File

@ -1,35 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-24T22:36:14Z
git_ref: 88d538e3
scope: code
substantive: true
raw_file: 20260824T223614Z_88d538e3_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing the shared
`Context.cancel()` deadline fix. The human accepted the proposed
child-reap bookkeeping invariant, asking only that the first fix receive
its own commit plan and commit before this update began.
## Response summary
Check that `ActorNursery` removes its paired reap-coordination entries
together while preserving valid pre-registration and immediate-cancel
paths. Extend the existing real-runtime reap tests to prove all three
child bookkeeping mappings are empty before `to_actor.run()` returns.
## Files changed
- `tractor/runtime/_supervise.py` - assert paired reap-map cleanup.
- `tests/test_to_actor.py` - verify graceful and hard-reap bookkeeping.
## Human edits
The human explicitly accepted this invariant update but directed the
preceding cancellation fix to be planned and committed as a separate
boundary first. No direct source-line edits were made by the human.

View File

@ -1,26 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-24T22:36:14Z
git_ref: 88d538e3
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 child-reap bookkeeping update after the
preceding `Context.cancel()` fix was committed separately.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
`ActorNursery._mark_child_reaped()` captures both reap-coordination
entries and asserts that they are either both present or both absent.
It intentionally does not require the reap-request event to be set,
because backend cancellation can reap immediately after registration.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Existing real-runtime graceful and hard-reap tests verify that
`ActorNursery._children`, `ActorNursery._child_reap_requests`, and
`ActorNursery._child_reaped` are all empty before the one-shot call
returns.
Run focused bookkeeping and real-runtime reap tests after the edit.

View File

@ -1,39 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-24T22:53:56Z
git_ref: 2f86dd1a
scope: code
substantive: true
raw_file: 20260824T225356Z_2f86dd1a_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing the paired
`ActorNursery` reap-state invariant. The human selected "keep" for the
reviewer's request to factor a duplicated debugger predicate in
`_try_cancel_then_kill()`.
## Response summary
Factor the child/tree debugger predicate into a local sampler used both
before and after the cancel-RPC checkpoint. Preserve dynamic debugger
lock re-evaluation and its distinction from root-wide debug mode.
## Files changed
- `tractor/runtime/_supervise.py` - factor the duplicated debugger
predicate without changing cancellation behavior.
## Human edits
The human explicitly selected "keep" after receiving keep/defer/drop
options for this isolated review item. During commit-plan review, the
agent found that a single pre-checkpoint snapshot could become stale;
the human selected a local helper which re-evaluates the lock after the
cancel RPC. The human then considered moving the predicate into
`.devx.debug` and accepted keeping it local after confirming that no
existing helper shares its supervisor-owned semantics. No direct
source-line edits were made by the human.

View File

@ -1,18 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-24T22:53:56Z
git_ref: 2f86dd1a
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 review refactor in
`_try_cancel_then_kill()` without changing debugger behavior.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
Compute the child/tree debugger predicate once, reuse it in the broader
hard-kill protection predicate, and pass it directly to
`debug.maybe_wait_for_debugger()`.
Run focused debugger/cancellation coverage and lint after the edit.

View File

@ -1,36 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-24T23:39:57Z
git_ref: 5327b25e
scope: code
substantive: true
raw_file: 20260824T233957Z_5327b25e_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing the debugger-state
sampler. The human selected "keep" for the paired review request to use
`Aid` objects as keys in the newly added reap-coordination maps.
## Response summary
Migrate only `ActorNursery._child_reap_requests` and
`ActorNursery._child_reaped` to `Aid` keys. Preserve the legacy
`ActorNursery._children` `.uid` key and pass full actor identities
through the narrow process-monitor bookkeeping path.
## Files changed
- `tractor/runtime/_supervise.py` - key fresh reap maps by `Aid`.
- `tractor/spawn/_spawn.py` - pass `Aid` into completed-reap cleanup.
- `tests/test_to_actor.py` - exercise `Aid` registration keys.
## Human edits
The human explicitly selected "keep" after reviewing the scope,
performance, and mutability tradeoffs. The human retained the legacy
tuple key for `_children` and accepted `Aid` for only the two fresh
private mappings. No direct source-line edits were made by the human.

View File

@ -1,27 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-24T23:39:57Z
git_ref: 5327b25e
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 review update which uses `Aid` keys for
the two fresh `ActorNursery` reap-coordination maps while preserving the
legacy `.uid` key for `ActorNursery._children`.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
Type and access `_child_reap_requests` and `_child_reaped` by `Aid`.
Pass full actor identities through registration, cancellation, and
completed-reap bookkeeping, deriving `.uid` only for `_children`.
> `git diff HEAD~1..HEAD -- tractor/spawn/_spawn.py`
Forward `subactor.aid` when publishing completed process teardown.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Update deterministic registration tests to exercise `Aid` map keys.
Run focused registration/reaping tests and the full `to_actor` suite.

View File

@ -1,38 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-25T01:57:42Z
git_ref: e42ecb55
scope: code
substantive: true
raw_file: 20260825T015742Z_e42ecb55_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing the `Aid` reap-map
migration. The human selected "keep" for comments explaining why both
spawn backends provisionally register children with `portal=None`.
## Response summary
Document that a child has no `Portal` until its IPC handshake yields a
`Channel`, and make `portal=None` explicit at both registration calls.
Identify the later replacement of each provisional entry with
`Portal(chan)`. Update the MP registration test double to accept and
assert the explicit provisional portal state. Name every registration
argument consistently in both backends.
## Files changed
- `tractor/spawn/_mp.py` - clarify provisional MP registration.
- `tractor/spawn/_trio.py` - clarify provisional Trio registration.
- `tests/test_to_actor.py` - model explicit provisional registration.
## Human edits
The human explicitly selected "keep" after receiving keep/defer/drop
options for this paired clarification. During local review, the human
then requested that `subactor` and `proc` also be passed by name in both
backend calls. No direct source-line edits were made by the human.

View File

@ -1,21 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-25T01:57:42Z
git_ref: e42ecb55
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 clarification for provisional child
registration in both process-spawn backends.
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
Explain that `portal=None` is provisional because no `Portal` can exist
until the child completes its IPC handshake and returns a `Channel`.
Use an explicit keyword argument and identify the later replacement with
`Portal(chan)`.
Run lint and the full `to_actor` runtime suite.

View File

@ -1,32 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-25T02:13:19Z
git_ref: ce430fca
scope: code
substantive: true
raw_file: 20260825T021319Z_ce430fca_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing provisional child
registration clarifications. The human selected "keep" for inlining the
guarded `functools.Placeholder` lookup with a walrus assignment.
## Response summary
Remove the standalone placeholder assignment and bind the optional
Python 3.14 sentinel directly in the existing conditional while
preserving compatibility behavior.
## Files changed
- `tractor/to_actor/_api.py` - inline placeholder feature detection.
## Human edits
The human explicitly selected "keep" after receiving keep/defer/drop
options for this isolated cleanup. No direct source-line edits were made
by the human.

View File

@ -1,19 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-25T02:13:19Z
git_ref: ce430fca
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 review cleanup for Python 3.14 partial
placeholder detection.
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py`
Inline the guarded `functools.Placeholder` lookup into the existing
condition with a walrus assignment, preserving fallback behavior when
the attribute is unavailable.
Run partial/placeholder normalization tests and the full `to_actor`
suite.

View File

@ -1,7 +0,0 @@
NOTE: you MUST pause this work at 12:50PM EST (BEFORE your weekly
limit reset) for review by a human!
---
attempt to resolve https://github.com/goodboy/tractor/issues/477
do it with /open-wkt.

View File

@ -37,6 +37,7 @@ Spawning actors
.. autoclass:: ActorNursery .. autoclass:: ActorNursery
:members: start_actor, :members: start_actor,
run_in_actor,
cancel, cancel,
cancel_called, cancel_called,
cancelled_caught cancelled_caught
@ -45,25 +46,11 @@ Spawning actors
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the :meth:`ActorNursery.start_actor` (daemon actor + portal) is the
blessed spawning primitive; pair it with blessed spawning primitive; pair it with
:meth:`Portal.open_context` for SC-linked remote tasks. ``Portal.open_context()`` for SC-linked remote tasks.
:meth:`ActorNursery.run_in_actor` is a *convenience* one-shot —
One-shot task actors spawn, run a single task, auto-cancel after the result — slated
-------------------- to be rebuilt as a high-level wrapper, so don't design around
it as the core model.
.. autofunction:: tractor.to_actor.run
.. note::
Without ``portal=``, :func:`tractor.to_actor.run` (parlance of
``trio.to_thread.run_sync()`` and friends) is the convenience
one-shot: spawn, run one task, block on its result and reap. It
combines :meth:`ActorNursery.start_actor`, a linked
:meth:`Portal.open_context` call and per-child reaping. With
``portal=`` it owns only the linked task and leaves the existing
actor's lifetime to the portal owner; that actor must expose both
the target module and ``tractor.to_actor.MODULE``. It supersedes
the removed (legacy, non-blocking)
``ActorNursery.run_in_actor()``.
.. deprecated:: 0.1.0a6 .. deprecated:: 0.1.0a6
@ -84,12 +71,14 @@ flowing back `exactly like trio`_.
:members: run, :members: run,
run_from_ns, run_from_ns,
open_stream_from, open_stream_from,
wait_for_result,
cancel_actor, cancel_actor,
chan chan
.. deprecated:: 0.1.0a6 .. deprecated:: 0.1.0a6
The str-form ``Portal.run('mod.path', 'fn_name')`` warns; ``Portal.result()`` warns; use :meth:`Portal.wait_for_result`.
The str-form ``Portal.run('mod.path', 'fn_name')`` also warns;
pass a function *object* whose module is listed in the target's pass a function *object* whose module is listed in the target's
``enable_modules``. ``Portal.channel`` is the legacy spelling ``enable_modules``. ``Portal.channel`` is the legacy spelling
of :attr:`Portal.chan`. of :attr:`Portal.chan`.

View File

@ -5,9 +5,8 @@ This is the curated reference for ``tractor``'s public surface: the
names you can import and lean on without reading runtime internals. names you can import and lean on without reading runtime internals.
Everything below is re-exported at the top level (``import Everything below is re-exported at the top level (``import
tractor``) unless a page says otherwise; subsystems like tractor``) unless a page says otherwise; subsystems like
``tractor.msg``, ``tractor.trionics``, ``tractor.to_actor``, ``tractor.msg``, ``tractor.trionics``, ``tractor.to_asyncio``,
``tractor.to_asyncio``, ``tractor.devx`` and ``tractor.log`` are ``tractor.devx`` and ``tractor.log`` are importable as submodules.
importable as submodules.
``tractor`` is "just trio_" extended across processes: every API ``tractor`` is "just trio_" extended across processes: every API
here is designed to keep the structured concurrency (SC) rules you here is designed to keep the structured concurrency (SC) rules you
@ -24,7 +23,6 @@ Most-used names at a glance:
open_root_actor open_root_actor
open_nursery open_nursery
to_actor.run
run_daemon run_daemon
ActorNursery ActorNursery
Portal Portal

View File

@ -30,12 +30,11 @@ Starting asyncio tasks from trio
.. note:: .. note::
:func:`open_channel_from` mirrors the :func:`open_channel_from` mirrors the
:meth:`tractor.Portal.open_context` handshake: the asyncio side calls ``Portal.open_context()`` handshake: the asyncio side calls
``chan.started_nowait(value)`` and that value pops out as ``chan.started_nowait(value)`` and that value pops out as
``first`` on the trio side. :func:`run_task` is the one-shot ``first`` on the trio side. :func:`run_task` is the one-shot
form — run a single asyncio-compatible coroutine fn and return form — run a single asyncio-compatible coroutine fn and return
its result to trio; :func:`tractor.to_actor.run` is its its result to trio.
cross-process sibling.
The inter-loop channel The inter-loop channel
---------------------- ----------------------

View File

@ -130,10 +130,9 @@ UDS: same-host, creds included
Pass ``enable_transports=['uds']`` and actors instead talk over Pass ``enable_transports=['uds']`` and actors instead talk over
unix-domain sockets, with socket files placed in the per-user unix-domain sockets, with socket files placed in the per-user
runtime dir: ``$XDG_RUNTIME_DIR/tractor/`` on linux, a short runtime dir (``$XDG_RUNTIME_DIR/tractor/`` on linux, the
owner-only ``/tmp/tractor-<uid>`` dir on Darwin, and the ``platformdirs`` equivalent elsewhere). Two perks over tcp on a
``platformdirs`` equivalent elsewhere. Two perks over tcp on a single single host:
host:
- no ports to fight over; addrs are just file paths, - no ports to fight over; addrs are just file paths,
- the kernel snitches on your peer for free: the listening side - the kernel snitches on your peer for free: the listening side

View File

@ -76,8 +76,8 @@ Just flip the flag on :meth:`tractor.ActorNursery.start_actor`:
infect_asyncio=True, infect_asyncio=True,
) )
The one-shot convenience ``tractor.to_actor.run()`` accepts the The one-shot convenience ``ActorNursery.run_in_actor()`` accepts
same flag. The ``to_asyncio`` APIs may **only** be called from the same flag. The ``to_asyncio`` APIs may **only** be called from
tasks inside an infected actor; calling them anywhere else raises tasks inside an infected actor; calling them anywhere else raises
a loud ``RuntimeError``. You can introspect at runtime with a loud ``RuntimeError``. You can introspect at runtime with
``tractor.current_actor().is_infected_aio()``. ``tractor.current_actor().is_infected_aio()``.
@ -229,7 +229,7 @@ dialog, skip the channel ceremony and use
It schedules the fn as an ``asyncio.Task``, waits for completion It schedules the fn as an ``asyncio.Task``, waits for completion
and hands the return value back to ``trio``; think of it as the and hands the return value back to ``trio``; think of it as the
cross-loop sibling of ``tractor.to_actor.run()``. Errors and cross-loop sibling of ``ActorNursery.run_in_actor()``. Errors and
cancellation are translated exactly as for channels. cancellation are translated exactly as for channels.
Cross-loop errors and cancellation Cross-loop errors and cancellation

View File

@ -64,13 +64,11 @@ What's going on here?
- three healthy actors are spawned as daemons via - three healthy actors are spawned as daemons via
:meth:`tractor.ActorNursery.start_actor`; left alone they'd :meth:`tractor.ActorNursery.start_actor`; left alone they'd
happily idle forever, happily idle forever,
- a fourth actor runs ``assert_err()`` via a blocking - a fourth actor runs ``assert_err()`` via ``.run_in_actor()`` and
``tractor.to_actor.run()`` one-shot and promptly trips its promptly trips its ``assert 0``,
``assert 0``,
- the resulting ``AssertionError`` ships back over IPC as a - the resulting ``AssertionError`` ships back over IPC as a
serialized error msg and re-raises *boxed* right at the call serialized error msg and re-raises *boxed* inside the nursery
inside the nursery block as a block as a :class:`tractor.RemoteActorError`,
:class:`tractor.RemoteActorError`,
- the nursery reacts like any ``trio`` nursery would: it cancels - the nursery reacts like any ``trio`` nursery would: it cancels
the three healthy siblings (graceful runtime-cancel requests, the three healthy siblings (graceful runtime-cancel requests,
acks awaited), reaps all four processes, then re-raises, acks awaited), reaps all four processes, then re-raises,
@ -230,23 +228,22 @@ Graceful first, hard as a last resort
The hard-kill path is *skipped* whenever an actor in the tree The hard-kill path is *skipped* whenever an actor in the tree
holds the debug-REPL lock (``debug_mode=True`` flavors): holds the debug-REPL lock (``debug_mode=True`` flavors):
Process signals raining down on a tree mid-``pdb`` session would SIGTERM raining down on a tree mid-``pdb`` session would
clobber your prompt. See :doc:`/guide/debugging`. clobber your prompt. See :doc:`/guide/debugging`.
Owned-child teardown in ``tractor`` begins with the same graceful Every process teardown in ``tractor`` walks the same escalation
steps, then selects the escalation path used by its supervisor, ladder, top rung first,
1. **graceful cancel request**: a runtime-cancel msg over IPC; the 1. **graceful cancel request**: a runtime-cancel msg over IPC; the
target actor cancels its tasks, closes its channels and exits target actor cancels its tasks, closes its channels and exits
its :func:`trio.run` cleanly, its :func:`trio.run` cleanly,
2. **soft wait**: the parent waits (bounded) for the child process 2. **soft wait**: the parent waits (bounded) for the child process
to exit on its own, to exit on its own,
3. **actor-nursery hard reap**: no cancel ack within the bounded wait 3. **SIGTERM**: no ack within the bounded wait (internally an
(internally an ``ActorTooSlowError``) escalates directly to ``ActorTooSlowError``) escalates to ``proc.terminate()``,
``proc.kill()`` before the child monitor joins the process, 4. **SIGKILL ultimatum**: still alive after the hard-kill timeout
4. **legacy soft-kill path**: older teardown callers may first issue (~1.6s)? The runtime logs that the "T-800" has been deployed to
``proc.terminate()`` and then deploy the "T-800" ``proc.kill()`` collect the zombie and issues ``proc.kill()``. No survivors.
ultimatum if the process survives that additional bounded wait.
The result is the **no-zombies guarantee**: ``tractor`` tries to The result is the **no-zombies guarantee**: ``tractor`` tries to
protect you from zombies, no matter what. Quoting the project protect you from zombies, no matter what. Quoting the project

View File

@ -62,10 +62,7 @@ one kwarg away,
.. code:: python .. code:: python
async with tractor.open_actor_cluster( async with tractor.open_actor_cluster(
modules=[ modules=['mylib.workers'],
'mylib.workers',
tractor.to_actor.MODULE,
],
count=4, count=4,
names=['scout', 'miner', 'smelter', 'smith'], names=['scout', 'miner', 'smelter', 'smith'],
debug_mode=True, # whole-fleet crash-to-REPL debug_mode=True, # whole-fleet crash-to-REPL
@ -73,12 +70,9 @@ one kwarg away,
... ...
From here the composition patterns are the usual ``tractor`` fare: From here the composition patterns are the usual ``tractor`` fare:
``portal.run()`` for bare one-shot RPCs (as in the demo), ``portal.run()`` for one-shot calls (as in the demo), or — for a
``tractor.to_actor.run(..., portal=portal)`` for cancellation-linked persistent bidirectional dialog per worker — concurrently enter N
one-shot tasks in an existing worker (include ``portal.open_context()`` blocks with
``tractor.to_actor.MODULE`` in ``modules``; the cluster still owns
the worker's lifetime), or — for a persistent bidirectional dialog
per worker — concurrently enter N ``portal.open_context()`` blocks with
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context` ``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
for that whole layer. for that whole layer.
@ -93,8 +87,8 @@ Clusters vs. nurseries
``open_actor_cluster()`` is sugar, not a new primitive: under the ``open_actor_cluster()`` is sugar, not a new primitive: under the
hood it's just :func:`tractor.open_nursery` plus N concurrent hood it's just :func:`tractor.open_nursery` plus N concurrent
:meth:`~tractor.ActorNursery.start_actor` calls plus a ``.cancel()`` ``start_actor()`` calls plus a ``.cancel()`` on the way out. Reach
on the way out. Reach for it when, for it when,
- you want a *flat*, homogeneous fleet (classic worker-pool or - you want a *flat*, homogeneous fleet (classic worker-pool or
map-style fan-out shapes), map-style fan-out shapes),

View File

@ -15,12 +15,12 @@ a single `structured concurrency`_ (SC) scope over IPC.
:alt: sequence diagram of the context handshake msg flow :alt: sequence diagram of the context handshake msg flow
Pretty much everything else is (or is slated to be) built on this Pretty much everything else is (or is slated to be) built on this
one primitive: ``tractor.to_actor.run()`` uses it for a linked one primitive: ``ActorNursery.run_in_actor()`` is a convenience
one-shot task, spawning and reaping an actor only when no ``portal=`` for "spawn, open a context, await the result, tear down"; plain
is supplied; plain ``Portal.run()`` RPC is planned to be ``Portal.run()`` RPC is planned to be re-implemented on top of it;
re-implemented on top of it; the multi-process debugger's tree-wide the multi-process debugger's tree-wide REPL lock rides one. Grok
REPL lock rides one. Grok this page and the rest of the library reads this page and the rest of the library reads as convenience
as convenience wrappers B) wrappers B)
The endpoint contract The endpoint contract
--------------------- ---------------------

View File

@ -44,9 +44,8 @@ clan shares one registry with zero config on your part.
The bootstrap rule inside ``open_root_actor()`` is delightfully The bootstrap rule inside ``open_root_actor()`` is delightfully
simple: simple:
- on boot, probe every addr in ``registry_addrs`` with a bounded - on boot, ping every socket addr in ``registry_addrs``; when none
Tractor ``Aid`` handshake; when none are passed the per-transport are passed the per-transport defaults are used: for TCP the
defaults are used: for TCP the
loopback ``('127.0.0.1', 1616)``, for UDS a loopback ``('127.0.0.1', 1616)``, for UDS a
``registry@1616.sock`` file, ``registry@1616.sock`` file,
@ -54,11 +53,9 @@ simple:
actor and register with the *existing* registry; your own IPC actor and register with the *existing* registry; your own IPC
server binds random same-transport addrs instead, server binds random same-transport addrs instead,
- if every address is absent, congratulations: you just became the - if **nothing answers, congratulations: you just became the
registrar. Your transport server binds the registry addrs registrar**. Your transport server binds the registry addrs
themselves and you start serving lookups for everyone else, themselves and you start serving lookups for everyone else.
- if no registrar answers but an address is occupied by a foreign or
non-responsive endpoint, startup fails instead of binding over it.
Pass ``ensure_registry=True`` when your program *requires* being Pass ``ensure_registry=True`` when your program *requires* being
the one-and-only registrar; boot then fails loudly with a the one-and-only registrar; boot then fails loudly with a
@ -199,10 +196,9 @@ the existing registrar:
trio.run(main) trio.run(main)
Per the bootstrap rules above, if those addrs are absent this process Per the bootstrap rules above, if the registrar at those addrs is
becomes its own registrar root, so the same code works standalone and *not* reachable this process simply becomes its own (registrar)
as a tree-joiner. An occupied address that does not complete a Tractor root — so the same code works standalone and as a tree-joiner.
registrar handshake fails startup instead of being rebound.
"Arbiter"? A legacy naming note "Arbiter"? A legacy naming note
------------------------------- -------------------------------

View File

@ -9,8 +9,8 @@ docs; what you read is what CI runs).
Roughly in "first date to long term relationship" Roughly in "first date to long term relationship"
order, order,
- :doc:`spawning` — actor nurseries, daemons, - :doc:`spawning` — actor nurseries, daemons +
``to_actor.run()`` one-shots and process lifetimes. one-shot workers, process lifetimes.
- :doc:`rpc` — portals: calling into another - :doc:`rpc` — portals: calling into another
process like it's a local ``await``. process like it's a local ``await``.
- :doc:`context` — the cross-actor task-pair - :doc:`context` — the cross-actor task-pair

View File

@ -119,16 +119,15 @@ Run a func in a process
Even a pool can be overkill; "run this one async func in a Even a pool can be overkill; "run this one async func in a
subprocess and give me the result" is a one-liner via subprocess and give me the result" is a one-liner via
:func:`tractor.to_actor.run`, :meth:`tractor.ActorNursery.run_in_actor`,
.. literalinclude:: ../../examples/parallelism/single_func.py .. literalinclude:: ../../examples/parallelism/single_func.py
:caption: examples/parallelism/single_func.py :caption: examples/parallelism/single_func.py
:language: python :language: python
``to_actor.run()`` is a *convenience wrapper* — spawn an actor, ``run_in_actor()`` is a *convenience wrapper* — spawn an actor, run
run exactly one task in it, block on and return its result, reap exactly one task in it, reap on result — not the core spawning
— not the core spawning model (that's model (that's :meth:`tractor.ActorNursery.start_actor` plus
:meth:`tractor.ActorNursery.start_actor` plus
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`). :meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
But for this fire-and-collect shape it's exactly the right amount But for this fire-and-collect shape it's exactly the right amount
of typing. of typing.

View File

@ -80,56 +80,28 @@ One special namespace exists: ``'self'`` resolves to the remote
how internal machinery (cancel requests, registry ops) travels; how internal machinery (cancel requests, registry ops) travels;
don't build your app on it. don't build your app on it.
One-shot subactors: ``to_actor.run()`` One-shot results: ``wait_for_result()``
-------------------------------------- ---------------------------------------
When the call should own a fresh subactor whose entire job is one A portal returned from
function call, :func:`tractor.to_actor.run` spawns it, runs the task, :meth:`~tractor.ActorNursery.run_in_actor` has exactly one
returns its result and reaps the process — all in one blocking call: "main" task running remotely; that task's ``return`` value is
delivered as the portal's *final result*:
.. code:: python .. code:: python
from functools import partial portal = await an.run_in_actor(fib, n=10)
final = await portal.wait_for_result()
final = await tractor.to_actor.run(
partial(fib, n=10),
an=an,
)
Semantics worth knowing: Semantics worth knowing:
- it blocks until the remote task returns, re-raising any - it blocks until the remote task returns, re-raising any
remote error in the usual boxed form right in the calling remote error in the usual boxed form.
task. - once resolved it's idempotent: later calls return the same
- lifetime mode also determines process ownership: ``an=`` spawns and cached value.
reaps a fresh child in an existing actor nursery, while passing - a *daemon* portal (from ``start_actor()``) has no main task,
neither does the same in a private call-scoped nursery (booting so there's no final result to wait for: you'll get a warning
the runtime if needed). ``portal=`` instead runs one linked task plus a ``NoResult`` sentinel. Results of individual daemon
in an existing actor; it neither spawns nor reaps that actor, so calls come straight back from each ``await portal.run()``.
the portal's owner remains responsible for its lifetime.
- concurrency composes the plain ``trio`` way: schedule
multiple ``run()`` calls into a local task nursery (see
``examples/parallelism/concurrent_toactor_primes.py``).
A reused actor must expose both the target module and the
``to_actor`` context trampoline:
.. code:: python
async with tractor.open_nursery() as an:
portal = await an.start_actor(
'worker',
enable_modules=[
__name__,
tractor.to_actor.MODULE,
],
)
try:
final = await tractor.to_actor.run(
partial(fib, n=10),
portal=portal,
)
finally:
await portal.cancel_actor()
Pure RPC daemons: ``run_daemon()`` Pure RPC daemons: ``run_daemon()``
---------------------------------- ----------------------------------
@ -175,8 +147,7 @@ call tears down the entire sub-tree — SC, transitively.
When to graduate to ``Context`` When to graduate to ``Context``
------------------------------- -------------------------------
The :meth:`~tractor.Portal.run` method is great for one-shot, ``portal.run()`` is great for one-shot, request-response calls.
request-response calls.
Reach for :meth:`~tractor.Portal.open_context` with an Reach for :meth:`~tractor.Portal.open_context` with an
``@tractor.context`` endpoint as soon as you want: ``@tractor.context`` endpoint as soon as you want:
@ -189,15 +160,10 @@ Reach for :meth:`~tractor.Portal.open_context` with an
:meth:`~tractor.Portal.cancel_actor` nukes the **entire** :meth:`~tractor.Portal.cancel_actor` nukes the **entire**
remote runtime and its process. remote runtime and its process.
:func:`tractor.to_actor.run` already enters the full In fact the source plans for ``Portal.run()`` itself to be
:meth:`~tractor.Portal.open_context` lifecycle. The older rebuilt on top of ``open_context()`` — contexts *are* the core
:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned inter-actor protocol. Take the full tour in
by the lower-level ``Actor.start_remote_task()`` directly, avoiding a :doc:`/guide/context`.
``Started`` handshake but owning less lifecycle machinery. A follow-up
should factor their shared linked-task lifecycle without requiring
``Portal.run()`` to delegate through the public context API or add
another wire message. Take the full tour in
:doc:`the context guide </guide/context>`.
.. seealso:: .. seealso::

View File

@ -91,34 +91,31 @@ somebody-ing:
What's going on here? What's going on here?
- :meth:`~tractor.ActorNursery.start_actor` forks off - ``start_actor('frank', enable_modules=[__name__])`` forks off
a new process, boots a ``tractor`` runtime inside it, and a new process, boots a ``tractor`` runtime inside it, and
allows it to serve functions from the current module (see the allows it to serve functions from the current module (see the
allowlist section below). allowlist section below).
- each :meth:`~tractor.Portal.run` call schedules a *new* task in - each ``await portal.run(...)`` schedules a *new* task in
frank's task tree and waits on its result — the full RPC story frank's task tree and waits on its result — the full RPC story
lives in :doc:`/guide/rpc`. lives in :doc:`/guide/rpc`.
- frank has no main task to complete, so without the final - frank has no main task to complete, so without the final
:meth:`~tractor.Portal.cancel_actor` call the nursery block would ``await portal.cancel_actor()`` the nursery block would wait
wait on him **forever**. Daemon lifetimes are *yours* to end; on him **forever**. Daemon lifetimes are *yours* to end; that
that explicitness is the point. explicitness is the point.
``to_actor.run()``: quick one-shot parallelism ``run_in_actor()``: quick one-shot parallelism
---------------------------------------------- ----------------------------------------------
Without ``portal=``, :func:`tractor.to_actor.run` is the convenience :meth:`~tractor.ActorNursery.run_in_actor` is the convenience
wrapper: spawn an actor, run exactly one async function in it, block wrapper: spawn an actor, run exactly one async function in it,
on the result, then reap the process — the distributed sibling of then reap the process as soon as the result arrives.
``trio.to_thread.run_sync()``.
.. code:: python .. code:: python
async with ( async with tractor.open_nursery() as an:
tractor.open_nursery() as an, portal = await an.run_in_actor(burn_cpu)
trio.open_nursery() as tn,
):
# burn rubber in the parent too... # burn rubber in the parent too...
tn.start_soon(burn_cpu) await burn_cpu()
total = await tractor.to_actor.run(burn_cpu, an=an) total = await portal.wait_for_result()
A few details worth knowing: A few details worth knowing:
@ -126,61 +123,43 @@ A few details worth knowing:
``name='something_cuter'``. ``name='something_cuter'``.
- the function's module is auto-added to the child's - the function's module is auto-added to the child's
``enable_modules`` allowlist. ``enable_modules`` allowlist.
- targets cross IPC as ``module:name`` references, so portable calls - extra ``**kwargs`` are forwarded to the function itself.
use module-global async functions or ``functools.partial`` objects - the child is *auto-cancelled* once its "main" result lands;
wrapping them. Nested functions, methods and callable objects do not at nursery exit these run-once children are always reaped
provide that stable address. first (causality_ is paramount!).
- target arguments are positional; use ``functools.partial()``
to bind target keyword arguments. Keywords passed directly to
``run()`` configure actor placement and spawning.
- the call blocks until the result (or error) lands and the
child is *auto-cancelled* (reaped) right after — so remote
errors raise directly in your calling task (causality_ is
paramount!).
- "placement" composes: ``an=`` spawns a call-owned child from an
existing actor nursery, while passing neither opens a private
call-scoped nursery. ``portal=`` instead reuses an existing actor:
the call scopes only its linked remote task, neither spawns nor
reaps the actor, and leaves its lifetime with the portal's owner.
That actor must expose both the target module and
``tractor.to_actor.MODULE``.
.. note:: .. note::
:func:`tractor.to_actor.run` is a convenience, **not** the core ``run_in_actor()`` is a convenience, **not** the core model.
model. For actor-owning placements it combines The source literally marks it for an eventual rebuild as
:meth:`~tractor.ActorNursery.start_actor`, a linked a thin "hilevel" wrapper on top of
:meth:`~tractor.Portal.open_context` call, and per-child :meth:`~tractor.Portal.open_context` (the modern inter-actor
cancellation/reaping. With ``portal=`` it uses only the linked task API). Teach your fingers to use it for quick
context call and leaves the existing actor's lifetime untouched. fire-and-collect parallelism — think a per-function
Teach your fingers to use it for quick trio-parallel_ style one-shot — and reach for
fire-and-collect parallelism — think a per-function trio-parallel_ ``start_actor()`` + ``open_context()`` for anything
style one-shot — and reach for long-lived, stateful or streaming
:meth:`~tractor.ActorNursery.start_actor` plus (:doc:`/guide/context`).
:meth:`~tractor.Portal.open_context` for anything long-lived,
stateful or streaming; see :doc:`/guide/context`.
Actor lifetimes and teardown order Actor lifetimes and teardown order
---------------------------------- ----------------------------------
There are two actor-lifetime flavors: So we have two lifetime flavors:
- **call-owned one-shot** (``to_actor.run()`` without ``portal=``): - **run-once** (``run_in_actor()``): lives exactly as long as
spawned for one task, then cancelled and joined before ``run()`` its single task; reaped the moment its result (or error)
returns its result or raises its error. arrives.
- **caller-owned daemon** (:meth:`~tractor.ActorNursery.start_actor`), - **daemon** (``start_actor()``): lives until *someone* cancels
including an actor later reused through it — an explicit ``await portal.cancel_actor()``, a bulk
``to_actor.run(..., portal=portal)``: lives until *someone* ``await an.cancel()``, or the one-cancels-all strategy kicking
cancels it via an explicit in on error.
:meth:`~tractor.Portal.cancel_actor`, a bulk
:meth:`~tractor.ActorNursery.cancel`, or the one-cancels-all
strategy kicking in on error.
On a clean exit of the nursery block the teardown order is: On a clean exit of the nursery block the teardown order is:
1. call-owned actors do not survive their own ``to_actor.run()`` 1. the nursery waits on every run-once actor's final result;
calls; each is reaped before its call returns. any errors from these are raised immediately so your code
2. the nursery waits on caller-owned daemon actors (acting as supervisor) gets first crack at handling them.
**indefinitely**. If you spawned one, you own its lifetime. 2. then it waits on daemon actors — **indefinitely**. If you
spawned a daemon, you own its lifetime.
When a child *is* cancelled, teardown is graceful-first per SC When a child *is* cancelled, teardown is graceful-first per SC
discipline: the runtime sends an IPC cancel request and gives discipline: the runtime sends an IPC cancel request and gives

View File

@ -185,10 +185,8 @@ first with a bounded grace window — so actor runtimes can run
their ``trio`` teardown paths — escalating to ``SIGKILL`` only as their ``trio`` teardown paths — escalating to ``SIGKILL`` only as
a last resort. The ``--shm`` sweep unlinks ``/dev/shm/`` segments a last resort. The ``--shm`` sweep unlinks ``/dev/shm/`` segments
that no live process has open (it leans on psutil_, already in that no live process has open (it leans on psutil_, already in
your dev venv, to check live mappings and fds) and ``--uds`` clears your dev venv, to check live mappings and fds) and ``--uds``
dead-binder sockets from Tractor's platform-specific runtime dir. It clears socket files whose binder pid is dead.
also unconditionally removes ``registry@1616.sock``; do not run the UDS
sweep while a live registrar is serving from that default address.
Testing your own ``tractor`` app Testing your own ``tractor`` app
-------------------------------- --------------------------------

View File

@ -43,21 +43,24 @@ Run it::
What's going on here? What's going on here?
- ``trio.run(main)`` starts the **root actor**; the ``tractor`` - ``trio.run(main)`` starts the **root actor**; the ``tractor``
runtime boots *implicitly* inside this ``tractor.to_actor.run()`` runtime boots *implicitly* inside ``tractor.open_nursery()``
call because neither ``an=`` nor ``portal=`` was supplied. No whenever it isn't already up. No special entrypoint, no
special entrypoint, no framework takeover - it's just a ``trio`` framework takeover - it's just a ``trio`` app,
app,
- inside ``main()`` a *subactor* is spawned via - inside ``main()`` a *subactor* is spawned via
``tractor.to_actor.run()`` and told to run exactly one ``ActorNursery.run_in_actor()`` and told to run exactly one
function: ``cellar_door()``, function: ``cellar_door()``,
- you get back a ``Portal``: your handle for invoking tasks in
the new process's (separate!) memory domain. We lean on it
much harder in the next section,
- the subactor, *some_linguist*, boots a fresh ``trio.run()`` in - the subactor, *some_linguist*, boots a fresh ``trio.run()`` in
a **new process** and executes ``cellar_door()`` as its linked a **new process** and executes ``cellar_door()`` as its *main
one-shot task (note the child proving it is *not* the root with task* (note the child proving it is *not* the root with
``tractor.is_root_process()``), then ships the return value ``tractor.is_root_process()``), then ships the return value
back over IPC, back over IPC,
- the call *blocks* until that final result arrives, then - the parent grabs that *final result* with
returns it - causality is preserved: your task only proceeds ``await portal.wait_for_result()``, much like you'd expect
once the child is *done*, dead, and reaped. from a "future" - except causality is preserved: the nursery
block only exits once the child is *done*, dead, and reaped.
.. margin:: Just need a worker pool? .. margin:: Just need a worker pool?
@ -68,20 +71,17 @@ What's going on here?
.. note:: .. note::
Without ``portal=``, ``to_actor.run()`` (parlance of ``run_in_actor()`` is the *convenience* wrapper: one-shot
``trio.to_thread`` and friends) is the *convenience* wrapper: spawn-run-reap semantics for when a subactor's entire job is
one-shot spawn-run-reap semantics for when a subactor's entire a single function call. The core primitives are
job is a single function call. The core primitives are ``ActorNursery.start_actor()`` (next up) paired with
:meth:`~tractor.ActorNursery.start_actor` (next up) — which ``Portal.open_context()`` for full, SC-linked cross-actor
hands you a ``Portal``, your handle for invoking tasks in the dialogs - see :doc:`/guide/context`.
new process's (separate!) memory domain — paired with
:meth:`~tractor.Portal.open_context` for full, SC-linked
cross-actor dialogs; see :doc:`/guide/context`.
Daemon actors and RPC Daemon actors and RPC
--------------------- ---------------------
A subactor spawned by ``to_actor.run()`` terminates after its lone A ``run_in_actor()``-spawned actor terminates when its main task
task returns. But often you want long-lived *daemon* actors instead: returns. But often you want long-lived *daemon* actors instead:
spawned once, then serving (allowlisted) RPC requests until told spawned once, then serving (allowlisted) RPC requests until told
otherwise. That's ``start_actor()``: otherwise. That's ``start_actor()``:
@ -91,17 +91,14 @@ otherwise. That's ``start_actor()``:
Two lifetime rules to internalize: Two lifetime rules to internalize:
- a subactor spawned and owned by ``to_actor.run()`` is cancelled - a ``run_in_actor()`` actor lives exactly as long as its main
and reaped before the call returns its result or raises its error, task; the nursery waits for that function (and thus the
process) to complete before unblocking,
- a ``start_actor()`` actor *lives forever* - an RPC daemon the - a ``start_actor()`` actor *lives forever* - an RPC daemon the
nursery will happily wait on **indefinitely** - until some nursery will happily wait on **indefinitely** - until some
task explicitly cancels it via ``Portal.cancel_actor()`` (as task explicitly cancels it via ``Portal.cancel_actor()`` (as
above), or its parent nursery is cancelled wholesale. above), or its parent nursery is cancelled wholesale.
Passing ``portal=`` is different: the call owns only the linked
remote task. It neither spawns nor reaps the existing actor; the
portal's owner must end that actor's lifetime.
.. tip:: .. tip::
Want your *entire program* to just be a long-lived RPC Want your *entire program* to just be a long-lived RPC
@ -211,20 +208,16 @@ The script of the scene (runtime ``INFO`` log lines trimmed)::
The new tricks in play: The new tricks in play:
- *donny* and *gretchen* start as daemon actors so each remains alive - two subactors, *donny* and *gretchen*, are each told to run
while the other discovers it and completes its line, ``say_hello()`` targeting the *other* by name,
- a local ``trio`` nursery runs both ``Portal.run(say_hello)`` calls
concurrently; starting both actors first avoids either reciprocal
dialog racing one-shot process reaping,
- ``tractor.wait_for_actor()`` blocks until the named peer has - ``tractor.wait_for_actor()`` blocks until the named peer has
registered with the tree's *registrar* (every actor announces registered with the tree's *registrar* (every actor announces
itself at boot), then yields a ``Portal`` connected itself at boot), then yields a ``Portal`` connected
**directly** to that peer, **directly** to that peer,
- each actor invokes its partner's ``hi()`` over that portal: - each actor invokes its partner's ``hi()`` over that portal:
actor-to-actor RPC with the root merely *directing* - and each actor-to-actor RPC with the root merely *directing* - and both
``Portal.run()`` returns its final line directly to ``main()``, final lines flow back to ``main()`` via
- the actor nursery explicitly cancels both daemons only after both ``await portal.wait_for_result()``,
dialogs complete,
- ``tractor.log.get_console_log("INFO")`` cranks up runtime - ``tractor.log.get_console_log("INFO")`` cranks up runtime
logging so you can watch the spawn/register/cancel machinery logging so you can watch the spawn/register/cancel machinery
narrate itself; remove it for a quiet set. narrate itself; remove it for a quiet set.

View File

@ -21,38 +21,22 @@ async def main():
"""Main tractor entry point, the "master" process (for now """Main tractor entry point, the "master" process (for now
acts as the "director"). acts as the "director").
""" """
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
print("Alright... Action!") print("Alright... Action!")
# both actors wait on (then dial!) the *other*, so each donny = await n.run_in_actor(
# must outlive both hellos: spawn as daemons, run the
# hellos concurrently, reap only once both complete.
portals: dict[str, tractor.Portal] = {
name: await an.start_actor(
name,
enable_modules=[__name__],
)
for name in ('donny', 'gretchen')
}
async def run_and_print(
name: str,
other_actor: str,
) -> None:
print(
# RPC through an existing actor's `Portal`.
await portals[name].run(
say_hello, say_hello,
other_actor=other_actor, name='donny',
# arguments are always named
other_actor='gretchen',
) )
gretchen = await n.run_in_actor(
say_hello,
name='gretchen',
other_actor='donny',
) )
print(await gretchen.wait_for_result())
async with trio.open_nursery() as tn: print(await donny.wait_for_result())
tn.start_soon(run_and_print, 'donny', 'gretchen')
tn.start_soon(run_and_print, 'gretchen', 'donny')
await an.cancel()
print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...") print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...")

View File

@ -10,14 +10,17 @@ async def cellar_door():
async def main(): async def main():
"""The main ``tractor`` routine. """The main ``tractor`` routine.
""" """
# spawn a subactor, run ``cellar_door()`` as its lone task, async with tractor.open_nursery() as n:
# block until its result arrives and the subactor is reaped.
print( portal = await n.run_in_actor(
await tractor.to_actor.run(
cellar_door, cellar_door,
name='some_linguist', name='some_linguist',
) )
)
# The ``async with`` will unblock here since the 'some_linguist'
# actor has completed its main task ``cellar_door``.
print(await portal.wait_for_result())
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -12,9 +12,9 @@ async def movie_theatre_question():
async def main(): async def main():
"""The main ``tractor`` routine. """The main ``tractor`` routine.
""" """
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
portal = await an.start_actor( portal = await n.start_actor(
'frank', 'frank',
# enable the actor to run funcs from this current module # enable the actor to run funcs from this current module
enable_modules=[__name__], enable_modules=[__name__],

View File

@ -15,9 +15,9 @@ async def stream_forever() -> AsyncIterator[int]:
async def main(): async def main():
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
portal = await an.start_actor( portal = await n.start_actor(
'donny', 'donny',
enable_modules=[__name__], enable_modules=[__name__],
) )

View File

@ -1,5 +1,3 @@
from functools import partial
import trio import trio
import tractor import tractor
@ -23,41 +21,26 @@ async def breakpoint_forever():
async def spawn_until(depth=0): async def spawn_until(depth=0):
""""A nested nursery that triggers another ``NameError``. """"A nested nursery that triggers another ``NameError``.
""" """
async with ( async with tractor.open_nursery() as n:
tractor.open_nursery() as an,
trio.open_nursery() as tn,
):
if depth < 1: if depth < 1:
tn.start_soon( await n.run_in_actor(breakpoint_forever)
partial(
tractor.to_actor.run,
breakpoint_forever,
an=an,
)
)
# Let the background one-shot enter `breakpoint_forever()` p = await n.run_in_actor(
# before its sibling raises and cancellation propagates. name_error,
name='name_error'
)
await trio.sleep(0.5) await trio.sleep(0.5)
# rx and propagate error from child # rx and propagate error from child
await tractor.to_actor.run( await p.result()
name_error,
an=an,
name='name_error',
)
else: else:
# recusrive call to spawn another process branching layer of # recusrive call to spawn another process branching layer of
# the tree; blocks (up) each level until the leaf's # the tree
# `name_error` relays through.
depth -= 1 depth -= 1
await tractor.to_actor.run( await n.run_in_actor(
partial(
spawn_until, spawn_until,
depth=depth, depth=depth,
),
an=an,
name=f'spawn_until_{depth}', name=f'spawn_until_{depth}',
) )
@ -82,37 +65,34 @@ async def main():
python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...) python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
""" """
async with ( async with tractor.open_nursery(
tractor.open_nursery(
debug_mode=True, debug_mode=True,
loglevel='pdb', loglevel='pdb',
) as an, ) as n:
trio.open_nursery() as tn,
): # spawn both actors
# spawn both spawner trees as concurrent one-shots; the portal = await n.run_in_actor(
# first tree's (relayed) error cancels the other.
tn.start_soon(
partial(
tractor.to_actor.run,
partial(
spawn_until, spawn_until,
depth=3, depth=3,
),
an=an,
name='spawner0', name='spawner0',
) )
) portal1 = await n.run_in_actor(
tn.start_soon(
partial(
tractor.to_actor.run,
partial(
spawn_until, spawn_until,
depth=4, depth=4,
),
an=an,
name='spawner1', name='spawner1',
) )
)
# TODO: test this case as well where the parent don't see
# the sub-actor errors by default and instead expect a user
# ctrl-c to kill the root.
with trio.move_on_after(3):
await trio.sleep_forever()
# gah still an issue here.
await portal.result()
# should never get here
await portal1.result()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -15,12 +15,12 @@ async def name_error():
async def spawn_error(): async def spawn_error():
""""A nested nursery that triggers another ``NameError``. """"A nested nursery that triggers another ``NameError``.
""" """
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
return await tractor.to_actor.run( portal = await n.run_in_actor(
name_error, name_error,
an=an,
name='name_error_1', name='name_error_1',
) )
return await portal.result()
async def main(): async def main():
@ -38,36 +38,29 @@ async def main():
- root actor should then fail on assert - root actor should then fail on assert
- program termination - program termination
""" """
async with ( async with tractor.open_nursery(
tractor.open_nursery(
debug_mode=True, debug_mode=True,
loglevel='devx', loglevel='devx',
) as an, ) as n:
trio.open_nursery() as tn,
):
# spawn both actors..
portal = await an.start_actor(
'name_error',
enable_modules=[__name__],
)
portal1 = await an.start_actor(
'spawn_error',
enable_modules=[__name__],
)
# ..and bg-schedule their erroring tasks. # spawn both actors
tn.start_soon(portal.run, name_error) portal = await n.run_in_actor(
tn.start_soon(portal1.run, spawn_error) name_error,
name='name_error',
# yield to the bg tasks so both RPC requests are )
# submitted (and start crashing) before the root's own portal1 = await n.run_in_actor(
# error below (the legacy `run_in_actor()` submitted spawn_error,
# in-line with each spawn). name='spawn_error',
await trio.sleep(0.5) )
# trigger a root actor error # trigger a root actor error
assert 0 assert 0
# attempt to collect results (which raises error in parent)
# still has some issues where the parent seems to get stuck
await portal.result()
await portal1.result()
if __name__ == '__main__': if __name__ == '__main__':
trio.run(main) trio.run(main)

View File

@ -17,12 +17,12 @@ async def name_error():
async def spawn_error(): async def spawn_error():
""""A nested nursery that triggers another ``NameError``. """"A nested nursery that triggers another ``NameError``.
""" """
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
return await tractor.to_actor.run( portal = await n.run_in_actor(
name_error, name_error,
an=an,
name='name_error_1', name='name_error_1',
) )
return await portal.result()
async def main(): async def main():
@ -36,39 +36,17 @@ async def main():
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...) `-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
`-python -m tractor._child --uid ('name_error', '3391222c ...) `-python -m tractor._child --uid ('name_error', '3391222c ...)
""" """
errors: list[BaseException] = []
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
# loglevel='runtime', # loglevel='runtime',
) as an: ) as n:
async def run_and_collect(fn): # Spawn both actors, don't bother with collecting results
''' # (would result in a different debugger outcome due to parent's
One-shot whose (boxed) error is stashed instead of # cancellation).
raised so a sibling's crash never cancels the others await n.run_in_actor(breakpoint_forever)
before they've had their own debugger sessions (the await n.run_in_actor(name_error)
"collect all errors" the legacy `run_in_actor()` API await n.run_in_actor(spawn_error)
did implicitly at nursery teardown).
'''
try:
await tractor.to_actor.run(fn, an=an)
except tractor.RemoteActorError as rae:
errors.append(rae)
# Spawn all one-shot task actors, collecting (vs.
# raising) their errors.
async with trio.open_nursery() as tn:
tn.start_soon(run_and_collect, breakpoint_forever)
tn.start_soon(run_and_collect, name_error)
tn.start_soon(run_and_collect, spawn_error)
if errors:
raise BaseExceptionGroup(
'multi_subactors errored!',
errors,
)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -21,8 +21,8 @@ async def main() -> None:
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
) as an: ) as n:
portal = await an.start_actor( portal = await n.start_actor(
'ctx_child', 'ctx_child',
# XXX: we don't enable the current module in order # XXX: we don't enable the current module in order

View File

@ -6,14 +6,14 @@ async def die():
async def main(): async def main():
async with tractor.open_nursery() as an: async with tractor.open_nursery() as tn:
debug_actor = await an.start_actor( debug_actor = await tn.start_actor(
'debugged_boi', 'debugged_boi',
enable_modules=[__name__], enable_modules=[__name__],
debug_mode=True, debug_mode=True,
) )
crash_boi = await an.start_actor( crash_boi = await tn.start_actor(
'crash_boi', 'crash_boi',
enable_modules=[__name__], enable_modules=[__name__],
# debug_mode=True, # debug_mode=True,

View File

@ -1,5 +1,3 @@
from functools import partial
import trio import trio
import tractor import tractor
@ -12,17 +10,15 @@ async def name_error():
async def spawn_until(depth=0): async def spawn_until(depth=0):
""""A nested nursery that triggers another ``NameError``. """"A nested nursery that triggers another ``NameError``.
""" """
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
if depth < 1: if depth < 1:
await tractor.to_actor.run(name_error, an=an) # await n.run_in_actor('breakpoint_forever', breakpoint_forever)
await n.run_in_actor(name_error)
else: else:
depth -= 1 depth -= 1
await tractor.to_actor.run( await n.run_in_actor(
partial(
spawn_until, spawn_until,
depth=depth, depth=depth,
),
an=an,
name=f'spawn_until_{depth}', name=f'spawn_until_{depth}',
) )
@ -41,37 +37,28 @@ async def main():
python -m tractor._child --uid ('name_error', '6c2733b8 ...) python -m tractor._child --uid ('name_error', '6c2733b8 ...)
''' '''
async with ( async with tractor.open_nursery(
tractor.open_nursery(
debug_mode=True, debug_mode=True,
enable_transports=['uds'], # TODO, pass this via osenv? enable_transports=['uds'], # TODO, apss this via osenv?
loglevel='devx', # XXX, required for test! loglevel='devx', # XXX, required for test!
) as an, ) as n:
trio.open_nursery() as tn,
):
# spawn the deeper tree in the bg..
tn.start_soon(
partial(
tractor.to_actor.run,
partial(
spawn_until,
depth=1,
),
an=an,
name='spawner1',
)
)
# ..while blocking on the shallow (faster to fail) tree # spawn both actors
# whose propagated error triggers nursery cancellation. portal = await n.run_in_actor(
await tractor.to_actor.run(
partial(
spawn_until, spawn_until,
depth=0, depth=0,
),
an=an,
name='spawner0', name='spawner0',
) )
portal1 = await n.run_in_actor(
spawn_until,
depth=1,
name='spawner1',
)
# nursery cancellation should be triggered due to propagated
# error from child.
await portal.result()
await portal1.result()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -13,24 +13,17 @@ async def main():
simultaneously. simultaneously.
''' '''
async with ( async with tractor.open_nursery(
tractor.open_nursery(
debug_mode=True, debug_mode=True,
# loglevel='debug' # ?XXX required? # loglevel='debug' # ?XXX required?
) as an, ) as n:
trio.open_nursery() as tn,
): # spawn both actors
# spawn the actor.. portal = await n.run_in_actor(key_error)
portal = await an.start_actor(
'key_error',
enable_modules=[__name__],
)
print( print(
f'Child is up @ {portal.chan.aid.reprol()}' f'Child is up @ {portal.chan.aid.reprol()}'
) )
# ..then schedule its erroring task in the bg while the
# root blocks below.
tn.start_soon(portal.run, key_error)
# XXX: originally a bug caused by this is where root would enter # XXX: originally a bug caused by this is where root would enter
# the debugger and clobber the tty used by the repl even though # the debugger and clobber the tty used by the repl even though

View File

@ -74,11 +74,11 @@ async def cancelled_before_pause(
async def main(): async def main():
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
) as an: ) as n:
await tractor.to_actor.run( portal: tractor.Portal = await n.run_in_actor(
cancelled_before_pause, cancelled_before_pause,
an=an,
) )
await portal.wait_for_result()
# ensure the same works in the root actor! # ensure the same works in the root actor!
await pm_on_cancelled() await pm_on_cancelled()

View File

@ -58,8 +58,8 @@ async def main():
debug_mode=True, debug_mode=True,
enable_transports=[tpt], enable_transports=[tpt],
loglevel='devx', loglevel='devx',
) as an: ) as n:
p = await an.start_actor( p = await n.start_actor(
'bp_boi', 'bp_boi',
enable_modules=[__name__], enable_modules=[__name__],
) )

View File

@ -17,14 +17,12 @@ async def main():
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
loglevel='cancel', loglevel='cancel',
) as an: ) as n:
# parks awaiting a result which only arrives once the portal = await n.run_in_actor(
# user quits (`BdbQuit`s) the child's REPL loop.
await tractor.to_actor.run(
breakpoint_forever, breakpoint_forever,
an=an,
) )
await portal.wait_for_result()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -12,12 +12,16 @@ async def main():
) as an: ) as an:
# TODO: ideally the REPL arrives at this frame in the parent, # TODO: ideally the REPL arrives at this frame in the parent,
# ABOVE the @api_frame of `to_actor.run()` .. # ABOVE the @api_frame of `Portal.run_in_actor()` (which
# should eventually not even be a portal method ... XD)
# await tractor.pause() # await tractor.pause()
p: tractor.Portal = await an.run_in_actor(name_error)
# the one-shot blocks on the subactor's result so the # with this style, should raise on this line
# boxed `NameError` raises right here. await p.wait_for_result()
await tractor.to_actor.run(name_error, an=an)
# with this alt style should raise at `open_nusery()`
# return await p.wait_for_result()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -90,7 +90,7 @@ async def main() -> None:
# TODO: 3 sub-actor usage cases: # TODO: 3 sub-actor usage cases:
# -[x] via a `.open_context()` # -[x] via a `.open_context()`
# -[ ] via a `to_actor.run()` call # -[ ] via a `.run_in_actor()` call
# -[ ] via a `.run()` # -[ ] via a `.run()`
# -[ ] via a `.to_thread.run_sync()` in subactor # -[ ] via a `.to_thread.run_sync()` in subactor
async with p.open_context( async with p.open_context(

View File

@ -50,8 +50,8 @@ async def trio_to_aio_echo_server(
async def main(): async def main():
async with tractor.open_nursery() as an: async with tractor.open_nursery() as n:
p = await an.start_actor( p = await n.start_actor(
'aio_server', 'aio_server',
enable_modules=[__name__], enable_modules=[__name__],
infect_asyncio=True, infect_asyncio=True,

View File

@ -29,9 +29,9 @@ async def main() -> None:
)) ))
await proc.wait() await proc.wait()
# await trio.sleep_forever() # await trio.sleep_forever()
# async with tractor.open_nursery() as an: # async with tractor.open_nursery() as n:
# portal = await an.start_actor( # portal = await n.start_actor(
# 'rpc_server', # 'rpc_server',
# enable_modules=[__name__], # enable_modules=[__name__],
# ) # )

Some files were not shown because too many files have changed in this diff Show More