EOF-driven cancellation can be absorbed by the ctx nursery before
teardown sees a disconnected channel and skips its recorded error.
Route an unfinished transport fault through `wait_for_result()` so
it raises the original error without waiting on broken IPC or
manufacturing a cancellation requester.
Share `ContextStatus._outcome_is_unfinished()` between EOF
notification and ctx teardown. Both buffered `Return` msgs and
decoded falsey results remain completed outcomes; notification
requires no existing error while teardown raises the recorded one.
Deats,
- Reconcile the close example and advanced-fault matrix with prompt
fault propagation, preserving break order and pre-`Stop` cases.
- Check exception leaves, socket causes, cancellation provenance,
and post-close send/receive error precedence and identity. Keep
synthetic Ctrl-C for the healthy no-break cases.
- Cover buffered/consumed `None`, `False`, `0`, and empty-string
results across EOF, including final value and type preservation.
- Add `assert_child_reaped()` for retained process handles. Check
both backend completion and PID disappearance without cleanup.
(this patch was generated in some part by `opencode` using
`gpt-6-astra` (`openai`))
Use `Context.wait_for_result()` directly at the nine remaining
call sites, including the debugger's root stdio-lock handshake.
Preserve `hide_tb=False` in the payload-spec test and keep the
outcome-renaming TODO beside the supported API.
Update the API removal note, ctx-specific comments and drain
diagnostics, and include the API-removal Prompt-IO record.
Prompt-IO: ai/prompt-io/opencode/20260922_context_result_removal.md
(this patch was generated in some part by `opencode`
using `gpt-6-astra` (`openai`))
Add twenty cases around real ctx state, channels and Trio scopes,
with inert actor resources.
Cover independent status initialization and diagnostics, falsey
results, repeated waits and transport-vs-self-cancel detection.
Check first-fault delivery and shallow-frozen identity metadata.
Exercise self-cancel ack policy, existing and ancestor reasons,
and local validation provenance at the message-raising boundary.
Use `.wait_for_result()` so these tests also cover the subsequent
deprecated-wrapper removal. Test provenance is already recorded
with the preceding status implementation.
Prompt-IO: ai/prompt-io/opencode/20260919_context_fault_status.md
(this patch was generated in some part by `opencode`
using `gpt-6-astra` (`openai`))
Make `Context._status` the single store for outcome, cancellation
and handshake state. Migrate internal callers and existing test
assertions directly, without descriptors or duplicate fields.
Keep policy and live resources on `Context`.
Deats,
- Add shallow-frozen fault metadata in `ContextCancelReason`,
delivered via Trio's public cancellation reason API. Preserve
the first cancelling fault when the recorded error changes.
- Distinguish local transport/protocol faults from boxed remote
failures; type-check self-cancel acknowledgements before reading
remote-error metadata.
- Move `._final_result_is_set()` onto `ContextStatus` and migrate
its five production callers plus the existing stream assertion.
- Retain the human's `._scope_tn` rename and add status-only reprs.
Keep public APIs, including the deprecated `.result()` wrapper.
New status regression tests follow in their own commit.
Prompt-IO: ai/prompt-io/opencode/20260919_context_fault_status.md
(this patch was generated in some part by `opencode`
using `gpt-6-astra` (`openai`))
Require `trio>=0.34.0`, the version exercised with object-valued
`Cancelled.reason` payloads in the downstream ctx experiment.
Resolve `uv.lock` with `trio` at `0.34.0`; also synchronize the
existing `py-multibase` requirement. No other package version moves.
Prompt-IO: ai/prompt-io/opencode/20260919_trio_reason_minimum.md
(this patch was generated in some part by `opencode` using
`gpt-6-astra` (`openai`))
A late self-cancel can arrive after the RPC service nursery has
closed. Starting `Actor.cancel()` there raises before teardown can
cancel the shielded parent-channel loop or run lifetime callbacks.
Schedule one root-owned task to await `Actor.cancel(None)` before
cancelling the root scope. Deduplicate pending requests, and cancel
the root scope directly once runtime cancellation has completed.
Add a fresh-child regression for open and closed service nurseries
under native Trio and asyncio infection. Check repeated requests
before completion, after completion and from a lifetime callback;
prove all three creator-owned shm segments disappear before parent
nursery teardown can mask a failure. Include the session raw record
referenced by the structured provenance already in the parent.
Prompt-IO:
ai/prompt-io/opencode/20260916_ib_disconnect_cleanup.md
(this patch was generated in some part by `opencode`
using `gpt-6-astra` (`openai`))
Save the proposed transport-fault handling for discovered peers.
`process_messages()` reports EOF then exits, leaving parent ctx
owners blocked until another send detects the loss. Wake unfinished
owners on that exact channel with `TransportClosed`, including
owners paused at application checkpoints unrelated to IPC.
This scope-cancellation policy remains provisional pending the
transport contract discussion. Saving the impl and regression cases
on this branch does not settle that policy or imply acceptance.
Deats,
- Match `Context.chan` by identity and notify all unfinished parent
ctxs on the failed connection, leaving other channels alone.
- Preserve received outcomes and record buffered `Return` receipt
using the `Unresolved` sentinel before EOF can replace success.
- Keep local transport loss distinct from a remote cancellation
request: do not invent `Context._canceller` provenance.
- Preserve the original disconnect cause through owner cancellation
and unwinding in `Context._maybe_raise_remote_err()`.
Also,
- Exercise two owners sharing a discovered sibling channel, blocked
in stream receive or an application `Event`, plus a healthy peer
that continues to echo after fault handling.
- Synchronize EOF injection after startup and task quiescence.
Cover drained and buffered final results without timing sleeps.
- Regress cause preservation directly with a connection-reset cause
and a distinct owner-unwinding exception.
- Carry the staged structured session provenance unchanged. Its
broader cancellation observations do not expand this boundary;
the root-owned `cancel_soon()` fix remains outside this save.
Exact-tree project tests remain pending in the commit plan. Earlier
session results in the provenance log are not passes for this tree.
Prompt-IO: ai/prompt-io/opencode/20260916_ib_disconnect_cleanup.md
(this patch was generated in some part by `opencode`
using `gpt-6-astra` (`openai`))
An asyncio-side task can handle the issued `TrioTaskExited` and
return normally. With `suppress_graceful_exits=False`, the strict
reporting branch in `translate_aio_errors()` nevertheless tried
to raise `trio_to_raise=None`, replacing successful completion
with TypeError('exceptions must derive from BaseException').
Require an actual pending exception before that strict-policy
raise. Existing errors retain their reporting and cause handling;
strict reporting does not manufacture an error after a successful
asyncio-side return with no remaining trio-side error.
Deats,
- run the channel-exit matrix directly in an infected root as well
as through sequential TCP RPCs in an infected child;
- add direct/cached cases where the asyncio-side task suppresses
the issued TTE and returns, under graceful and strict policies,
with native and Greenback task execution;
- test a child suppressing TTE and staying blocked: check channel
startup state, then require receipt of that channel's exact
issued TTE with trio-side exit set and asyncio still running;
- publish the receipt marker only after those checks, before the
responsive parent awaits `an.cancel()`; require nursery exit
to join the retained child process handle;
- bound that probe from another process, failing if its watchdog
must rescue the test group; successful child exit does not prove
that hard-kill escalation was needed;
- explicitly `raise signal`, rename the caller-cancellation flag
to `cancel_trio_side`, and format signal/callback diagnostics;
- retain the maintainer's test layout and helper documentation.
Qualification: expanded matrix before the guard: 8 failed / 33
passed; all eight failures are strict-policy successful returns.
After the guard, selected exit/root/infected-asyncio/formatter
suites: 70 passed / 18 deselected, with 24 fork deprecation
warnings. Strengthened blocked-child state checks: 1 passed.
Interactive SIGINT/debugger cases remain excluded.
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
Exercise the exit-signal handling fixed in 4dfcf6f6 using the real
`to_asyncio.open_channel_from()` API in an infected child actor.
An expected `TrioTaskExited` must not escape graceful teardown or
be raised again as an unhandled asyncio done-callback exception.
Deats,
- run six cases with native and Greenback-wrapped asyncio tasks:
direct channel closure, last cached caller exit/cancellation,
strict TTE reporting, and distinct RuntimeError/TTE cleanup faults;
- use `block_until_trio_side_exit()` to record task identity,
complete startup, send one channel value and block until signalled;
- wrap the channel API in `maybe_open_context()` for cached cases,
proving overlapping `trio.Task`s receive the same channel;
- synchronize the second subscription with `tn.start()` and an
event, then join its task before checking asyncio is still alive;
- open two asyncio tasks sequentially, checking each finishes when
its channel closes and retains the expected exception identity;
- raise a NEW TTE during cleanup to reject blanket type-based
suppression, retaining the issued TTE as the new error's cause;
- drain a host-loop callback before checking unhandled exceptions
for expected shutdown, separately from actor/RPC usability;
- await two sequential `Portal.run()` calls in the same child,
then explicitly cancel the actor before leaving its nursery.
Keep the Piker reproduction as context rather than describing the
blocking asyncio task as a quote/feed implementation. Document case
selection, closure contracts and the assertions at their call sites.
Qualification: latest matrix 12 passed. Earlier combined asyncio
and task-formatter run: 41 passed / 18 deselected. This matrix does
not yet cover an infected root or an asyncio task suppressing TTE
and returning or remaining blocked; those are follow-up coverage.
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
In `.open_channel_from()`, when the `trio`-side parent task exits, it
signals the running `asyncio`-side task with `TrioTaskExited`; this is
our internal exit signal to any still-running `asyncio` task. When the
`asyncio`-side raises the issued signal, previously
`translate_aio_errors()` let it escape despite
`suppress_graceful_exits=True`, (in `piker`'s case) turning an expected
(and cached via `.trionics.maybe_open_context`) channel teardown into
a (quote-feed) task-restart failure.
Now if that task raises the same signal, `translate_aio_errors()`
suppresses it when `suppress_graceful_exits=True`, while preserving any
original `trio`-side error. There are two cooperating fixes:
1. `translate_aio_errors()` returns early without raising that TTE. This
prevents normal channel teardown from propagating it back up to
the originating task.
2. The `asyncio` done callback `signal_trio_when_done()` now returns
early for that same TTE ack and prevents it from
recancelling the already-unwinding `trio-side` or raising the signal
again as an unhandled callback error.
Deats,
- record the issued `TrioTaskExited` instance in
`LinkedTaskChannel._aio_owner_exit` before signalling the
`asyncio`-side task, retaining the original `trio`-side error
as its cause when present.
- use exception identity (`is`), rather than type, to recognize
that issued signal; a separately raised TTE or another
`asyncio`-side cleanup error retains existing error handling.
- in `translate_aio_errors()`, require the `trio`-side parent
task to have exited before handling the acknowledged signal:
- raise any original `trio_err` first, without making its own
exit signal its cause and creating an exception-cause cycle;
- OW return early under `suppress_graceful_exits=True`;
- with suppression disabled, raise the exact issued TTE.
- in `signal_trio_when_done()`, retrieve the task exception but
skip parent-task recancellation and callback error relay when
it is that issued TTE and the `trio`-side has already exited.
- in `wait_on_coro_final_result()`, retain the issued TTE in
`chan._aio_err` for the reporting decision above, logging its
receipt as an exit acknowledgement rather than a task failure.
- retain the actual `maybe_signal_aio_task()` delivery report
for both acknowledgement logs; include the `trio`-side parent
task via `pformat_task()`, its original error/cancellation cause,
and runtime-level diagnostics for each reporting decision.
Update docs/reporting,
- document the signal writer, readers and teardown outcomes;
- retain the actual signalling report for both acknowledgements;
- use `pformat_task()` to identify the `trio.Task` in diagnostics;
- distinguish the parent's error from its issued TTE;
- log suppression, strict reporting and `trio`-side-error precedence;
- clarify graceful suppression for cached-channel teardown;
- keep the queue-shutdown TODO beside its existing handler.
Qualification using a separate, uncommitted regression module:
- original matrix 8 failed / 4 passed; candidate matrix 12 passed.
- selected bridge/formatter run: 41 passed / 18 deselected.
- earlier `piker` real-channel integration: 4 passed.
- human reset traces acknowledge owner exit and preserve a quote
`TimeoutError`.
Prompt-IO: ai/prompt-io/20261028T193020Z_b64c89f0_cached_owner.md
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
A caller can depart after its endpoint finishes but before the final
`Return` reaches it. Letting that `TransportClosed` escape `_invoke()`
cancels unrelated RPCs in the actor's shared service nursery.
Guard only the final result send and report the actor, context, scope,
channel and transport error using existing primitive renderers. Keep
endpoint execution and nursery teardown outside that suppression.
Extend the real-TCP reproducer with a genuine endpoint-raised
`TransportClosed` identity control. Snapshot the endpoint, sibling and
nursery owner through the shared task formatter when cancellation
exposes a collateral failure.
Prompt-IO: ai/prompt-io/20261019T145431Z_0553fcf8_ctx_return_fix_prompt_io.md
Prompt-IO: ai/prompt-io/20261020T193619Z_0553fcf8_review_prompt_io.md
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
Add `pformat_task()` beside `pformat_task_uid()` so diagnostics can
inspect an explicit endpoint, sibling or nursery-owning `trio.Task`.
Keep the current-task default and decimal head/tail identity format.
Preserve the `tractor.log` entry point with lazy forwarding, avoiding
eager debugger initialization during logging imports. Move the
existing formatting TODOs with the implementation.
Exercise real parent/child tasks without an actor, including coroutine
identity, nursery ownership and equivalent log/tooling UID output.
Prompt-IO: ai/prompt-io/20261021T145344Z_0553fcf8_task_format_prompt_io.md
Prompt-IO: ai/prompt-io/20261021T151133Z_0553fcf8_uid_move_prompt_io.md
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
A chart disconnect can leave its context endpoint returning over
a closed channel. Exercise the real `_rpc._invoke()` dispatcher
with loopback TCP and a sibling service to isolate the resulting
`TransportClosed` nursery cancellation.
Deats,
- Order caller close, callee EOF and endpoint completion with
events instead of timing sleeps or injected transport errors.
- Keep an ordinary-RPC control for existing response hardening.
- Capture the collateral task, actor/PID, nursery, cancel scope
and cancellation source in a test-generated exception note.
- Record the live IB diagnosis and generated-test provenance.
The ordinary-RPC control passes; the context case remains red
with `ClosedResourceError` wrapped as `TransportClosed`. This
commit establishes the regression before runtime remediation.
Prompt-IO: ai/prompt-io/20261019T140536Z_9a0f220f_final_return_prompt_io.md
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
The `CUSTOM_LEVELS`-only lookup raised `KeyError` for built-in
log levels such as 'INFO'. Resolve other registered names through
`logging.getLevelNamesMapping()` only when the custom lookup
misses, without eagerly evaluating the fallback on custom hits.
Deats,
- keep custom precedence, case-insensitive names and `NOTSET=0`;
- preserve integer inputs and unknown-name `KeyError` behavior;
- keep logger-local and global `isEnabledFor()` disable controls;
- advertise integer inputs on the adapter's `at_least_level()`;
- add 62 raw/adapted regressions with explicit threshold examples
and fixture docs distinguishing instance and global log state.
The logging suite passed all 70 cases; the original Piker consumer
probe passed 8/8 with its string-level feed guard retained.
Prompt-IO: ai/prompt-io/20261011T053737Z_a966928d_prompt_io.md
(this patch was generated in some part by `opencode` using
`gpt-6.1-sol` (`openai`))
Keep this worktree's existing message archives on the legacy backend
and ignore private workflow runtime state and configuration.
(this commit message was generated by `opencode` using `gpt-6-astra` (`openai`))
Match deletion inputs to the canonical addresses stored by
`register_actor()`. Legacy TCP/UDS pairs and the `uds` alias
otherwise miss their entry and leave it registered.
Cover legacy and tagged declarations as tuples and msgpack-style
lists, checking the returned uid and removal of the final entry.
Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5408224830
(this patch was generated in some part by `opencode` using `gpt-6-astra` (`openai`))
Record `py-multibase` as a direct Tractor dependency, matching the WG
key-codec requirement already declared in `pyproject.toml`.
Also retain uv's Python 3.14 markers for `greenback` deps so the lock
matches the current py313-py314 project range.
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Separate legacy declaration inputs from canonical serialized outputs
in IPC and architecture docs, and state the same-version registrar
tree contract while no wire-format negotiation exists.
Add PR #505's feature news fragment for `TunnelledAddress` and tagged
TCP/Unix output.
Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Unpack TCP's canonical proto tag before selecting the overlay host,
then pass a tagged bind declaration to the child actor. The example
otherwise raises before `echo_srv` starts.
Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Store the validated `waddr.unwrap()` result so legacy and tagged
declarations share one registry identity. This lets stale-entry
eviction replace an older actor which used the compatibility form.
Cover TCP and UDS registrations crossing from legacy to tagged addrs.
Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Normalize implicit listener declarations through `wrap_address()`,
just like caller-supplied addrs. Tagged defaults otherwise reach
endpoint reflection as plain tuples and fail before socket startup.
Add a TCP/UDS regression which omits `accept_addrs` and proves
each generated ep holds the expected concrete `Address`.
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
- Make `TCPAddress.unwrap()` emit `('tcp', host, port)` and
`UDSAddress.unwrap()` emit `('unix', path)` while retaining the
compatibility readers from the preceding change.
- Pass concrete TCP fields to Trio, compose multiaddrs from tagged
values, and let `SpawnSpec` carry protocol-specific tuple shapes
for validation by `wrap_address()`.
- Compare runtime, registry, bind, and tunnel addresses through
canonical serialized forms and cover both TCP and UDS operation.
Prompt-IO: ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
- Define canonical `tcp` and `unix` tuple shapes while retaining
legacy pair aliases as the emitted `UnwrappedAddress`.
- Dispatch tagged tuple/list payloads explicitly, accept `uds` as a
Unix input alias, and preserve legacy TCP, UDS, and native IPv6
readers.
- Cover tag aliases, msgpack-style lists, legacy payloads, and IPv6
socket addresses before switching writers.
Prompt-IO: ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Separate serializable bindspace declarations from live namespace
identity, FDs, ownership and teardown resources.
Require child namespace entry during spawn bootstrap, before actor
runtime initialization, then distinguish listen/dial provisioning and
owned/borrowed cleanup without encoding operation role into maddrs.
Prompt-IO: ai/prompt-io/opencode/20260820T021516Z_dfad66a0_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Carry tunnel declarations through listener configuration, then strip
them immediately before constructing transport endpoints.
Also allocate random listener addresses from a contacted registry's
overlay, and prove a real TCP listener never stores the wrapper while
the source declaration retains its bindspace metadata.
Prompt-IO: ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Retain tunnel annotations through address declaration, then hand only
the bindable overlay to exact-type transport lookup and dialing.
Broaden `Channel.from_addr()` and `_connect_chan()` inputs accordingly,
and cover plain plus tunnelled TCP dispatch arguments.
Prompt-IO: ai/prompt-io/opencode/20260819T213144Z_f81fc5e5_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Drop the example-local address struct and hand-rolled single-tunnel
parser now that discovery owns the production implementation.
Keep only the explicit `wg(8)` peer probe in the multihost helper,
and update the examples and plan for nested parsing, packaged codec
dependencies and tractor-owned bindspace provisioning.
Prompt-IO: ai/prompt-io/opencode/20260818T075031Z_dd02c7c0_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Teach discovery to preserve WireGuard bearer and identity metadata
around a bindable TCP overlay.
Deats,
- encode `wg(8)` keys as strict 32-byte multibase values
- peel nested stacks with `Multiaddr.decapsulate_code()` and compose
them with `.encapsulate()` instead of splitting strings
- integrate wrappers with `parse_maddr()`, `mk_maddr()`,
`wrap_address()` and `parse_endpoints()`
- pin the unreleased py-multiaddr#108 codec in package metadata
- cover exact round trips, nesting, bad grammar and missing codecs
Prompt-IO: ai/prompt-io/opencode/20260818T075031Z_dd02c7c0_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Introduce the first layer-A address type from the `wg` bindspace
plan without treating a transparent tunnel as a `MsgTransport`.
Deats,
- add frozen `WGTunnelSpec` and `TunnelledAddress` structs which
delegate proto identity, bindspace, validity and wire
serialization to their overlay
- add `strip_tunnels()` and `tunnels_of()` for nested wrappers
- recognize wrappers in `is_wrapped_addr()` while keeping them out
of `_address_types`
- cover delegation, namespace fallback and nested peeling semantics
Also,
- widen `Address.namespace` ids for named netns
- export the new discovery API
- clarify that tractor's layer-C bindspace lifecycle may provision
the kernel-owned bearer without making it a `MsgTransport`
Prompt-IO: ai/prompt-io/opencode/20260818T021729Z_d9a6e2e9_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Validate WireGuard keys and tunnel descriptors strictly. Inspect
iface keys asynchronously by local/peer role and reject unsupported
nested tunnels.
Correct both host binds and service publication, document an
unprivileged two-host setup and pin the merged `py-multiaddr` codec
revision in the lock.
Prompt-IO: ai/prompt-io/opencode/20260831T022317Z_768b5316_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Bring the shared backend contract and TIPC plan in line with
current runtime behavior and downstream implementation evidence.
Rework the QUIC plan around actor-owned endpoint, bootstrap,
stream, listener and cleanup lifecycles, with explicit validation
gates for the still-unverified UniFFI details.
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
Replace mutable branch pointers with the exact source commit and
historical base-to-commit range. Normalize the substantive scope
while leaving the recorded raw response unchanged.
Prompt-IO: ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
§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`))
`py-multiaddr` already ships the entire tunnel compose/peel
surface and this module was reimplementing it — a raw
`maddr.split('/')` plus index arithmetic, sitting directly under
a comment congratulating itself for not hand-rolling a parser.
Same NIH trap gh #429 existed to close, just one layer up. The
API was linked from gh #443's own 2nd bullet the whole time.
So every cut now goes through the real thing,
| need | API |
| --- | --- |
| isolate the bearer | `.decapsulate_code(P_WG)` |
| per-seg maddrs | `.split()` |
| rejoin a seg tail | `Multiaddr.join()` |
| read the key | `.value_for_protocol('wg')` |
| recompose | `.encapsulate()` |
`.decapsulate_code()` turns out to handle the infix `/wg/` seg
cleanly *because* it cuts on proto-code and never tries to match
an addr value — the key seg has no addr of its own, which was
the exact thing I'd assumed would need bespoke handling.
Deats,
- rename the role fields `inner`/`inner_proto` ->
`overlay`/`overlay_proto`, matching `py-multiaddr`'s
encapsulation model (earlier segs wrap later ones) and #443's
owner table. `inner` collided head-on w/ call-stack `inner`,
where it reads as higher-up + later-called, while here the
encapsulated addr is bound *first* and sits deeper.
- drop `_segments()` and its degraded hand-split path entirely.
W/o the codec there's now one actionable `RuntimeError`
instead of a silent downgrade, superseding the swallow fix in
7d6e7955.
- add `.as_multiaddr()` so callers can stay in `Multiaddr` land;
`.maddr` is now just `str()` of it.
- accept `str|Multiaddr` on the way in.
- carry `bearer_ip`/`overlay_ip` so a v6 stack re-renders as v6
— the old `.maddr` hardcoded `/ip4/` and would silently
mangle it.
- both host scripts follow the rename to `.overlay`.
⚠️ `value_for_protocol('ip4')` on a *full* tunnelled maddr
silently returns the **first** match, i.e. the bearer's host, so
it's only ever called here on an already-peeled sub-maddr.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
it lands" framing in plan-03 and the example README was stale in
both directions: the branch pin is obsolete, yet you still can't
just `pip install multiaddr`.
Deats,
- §3.2's grammar table is now re-verified against the upstream
merge (`f86519da`) rather than only `baudco@wg_support` in a
throwaway venv. Also notes the codec enforces a 32-byte key,
so a truncated one is a `StringParseError` and not a silently
mangled parse.
- §1 says merged-but-unreleased; the still-open work is spec
registration (py-multiaddr#107 + gh #483).
- §3.4 swaps "pin the branch" for the `[tool.uv.sources]` `rev`
pin, and fixes the `_have_wg_maddr_proto()` recipe it
suggested — probing w/ `Multiaddr('/wg/uAAAA')` now ALWAYS
raises bc the codec wants 32B, i.e. that feature-detect would
report `False` even w/ the proto perfectly well known.
- risk table row goes "#108 not merged" -> "merged but
unreleased".
- example README: `uv sync` alone now suffices bc of the pin;
documents the 32B check and points at
`_have_wg_maddr_proto()` as the gate.
The one surviving `baudco` mention is deliberate, it records
where the grammar was *first* verified.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`_segments()` called `Multiaddr(maddr)` purely to validate, then
swallowed every failure under `except Exception: pass`. That was
harmless pre-#108 — w/o a `wg` codec there was nothing to
validate — but now that the codec is pinned in, the swallow is
load-bearing and disabled: a malformed key sails past validation
into `wg8_pubkey()`, which happily emits a corrupt b64 str, and
the returned struct then fails its own `.maddr` round-trip. No
raise, just quietly wrong output.
Deats,
- add `_have_wg_maddr_proto()`, the gate plan-03 already
referenced but which never actually existed. Impl'd as
`protocols.protocol_with_name('wg')` under
`except ProtocolNotFoundError` and cached in a mod global,
same shape as the TIPC plan's `is_tipc_available()`.
- only validate when that gate is `True`, and let
`StringParseError` propagate — a maddr which doesn't parse
must NOT reach `wg8_pubkey()`.
- keep the degraded split for a pre-#108 install, now w/ an
explicit `XXX` naming the validation you give up.
So parsing stays pure but becomes total-or-raises. Our own
`ValueError`s (missing `/wg/` seg, bare tunnel w/o an overlay
ep) are unaffected, as is the `wg(8)` b64 round-trip.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
py-multiaddr#108 (the `/wg/u<key>` maddr proto) merged upstream
on 2026-07-28 as `f86519da`, but ships in no release yet — the
latest `0.2.0` predates it by ~4 months and carries no `wg`
codec at all. So `examples/multihost/wg_lan/` can't parse its
own maddrs off PyPI.
Pinned by `rev` and not `branch` so CI stays reproducible. Note
the lock now records the git source *instead of* the `>=0.2.0`
specifier, i.e. the dep floor above is fully overridden for as
long as this pin lives.
TODO, drop the pin (and bump that floor) the moment a release
carries the codec; the only consumer is the `wg_lan` example
set.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
One record covering all 9 commits on this branch, per the NLNet
generative-AI policy and the existing `ai/prompt-io/claude/`
convention.
Uses diff-ref mode for both the plan docs and the example code
(`git diff main..ng_tpts_planning -- <path>`) rather than
duplicating content already in `git log -p`. Kept verbatim in
the `.raw.md`: the four verified findings (trio's
family-agnostic `SocketStream`/`SocketListener`, the round-trip
table proving `/wg/` is infix, the proto-key `UnwrappedAddress`
rationale, and `setns(2)`'s per-thread reality), since those are
reasoning rather than diffable output.
`## Human edits` records that the steering here was substantial
and mid-session rather than post-hoc: two model claims about wg
maddr semantics were challenged and retracted (incl. in an
already-posted issue comment), and the proto-key +
netns-as-runtime-config framings were human-directed. Also notes
the one model-initiated correction — a pre-publication
self-review that downgraded the `uniffi`/asyncio thesis and the
TIPC duplicate-binder claim to explicitly-flagged assumptions.
Prompt-IO: ai/prompt-io/claude/20260813T001102Z_27c34aeb_prompt_io.md
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
`tests/test_docs_examples.py` walks `examples/` **recursively**
and subproc-runs every collected file asserting `rc == 0`. Ran
its exact filter against the tree: all 4 of our files were being
collected — including `README.md`, since the filter never checks
the extension, so CI would have literally tried `python
README.md`. These need a real second host + a live `wg` tunnel,
so they can't ever satisfy that gate.
`'multihost' not in p[0]` is already in the test's exclusion
list w/ no dir yet using it, so this is a pure `git mv` — zero
test changes — and it's what the exclusion was plainly there
for. Collection drops 24 -> 20 files, 0 of them ours.
Also records *why* in the two places someone would look before
adding the next one: a callout at the top of the example README
and a note on plan 03's §3.4 deliverables. Anything needing a
second host or live tunnel goes under `examples/multihost/`.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.
- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
`bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
property that re-renders the canonical form, and pure
`mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
rejects #482's inverted suffix form w/ an actionable error and
stays **side-effect free** — `verify_wg_peer()` is a separate,
explicitly impure step the caller composes, never something a
parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
only `addr.inner` into `open_nursery()`/`open_root_actor()`,
which is the whole point — the bearer + key layers are already
established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
branch install line, tunnel setup, and a "what changed vs
#482" section enumerating the corrections.
Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
The prior revision (and gh #482's examples) had it as a suffix,
`/ip4/10.0.11.1/tcp/1616/wg/u<key>`. Wrong: verified against
`baudco/py-multiaddr@wg_support` (py-multiaddr#108) installed in
a throwaway venv, the canonical form is
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
where segs *before* `/wg/` are the **bearer** — the underlay
`(ip, udp-port)` `wg(8)` itself listens on (`ListenPort`), per
the codec docstring's own example — and segs *after* are the
**overlay** ep, the only part we ever bind. The suffix form does
parse, which is why it slipped through, but it's semantically
inverted: overlay addr where the bearer belongs, `tcp` where
wg's `udp` goes, and no overlay ep declared at all.
Records the observed `[p.name for p in m.protocols()]` lists so
the `match` can be written against fact, and replaces the
"composed vs not" framing w/ what's actually the design axis:
three parts, three **owners** — bearer bound by the kernel via
`wg-quick`/`pyroute2`, `/wg/u<key>` bound by nothing (it's an
identity, verified out-of-band), overlay bound by our
`IPCServer` as `.inner`. `_peel_tunnel_segs()` correspondingly
grows a 3rd return, splitting *at* the tunnel seg so nested
tunnels fall out for free.
Also hoists the netns conclusion to the top of §5.3 where it
can't be missed: netns is a **runtime-level config API, not an
actor-app-code one**. It's a spawn/boot-time input alongside
`enable_transports`/`tpt_bind_addrs`, deliberately w/ no
`await actor.enter_netns(...)`, because `setns(2)` neither moves
already-created sockets nor applies beyond the calling thread —
so a mid-life API would silently leave the IPC server bound in
the old ns.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Shape-matching in `wrap_address()` doesn't survive 4 backends and
the plans were papering over it: TIPC's natural unwrapped form is
a `(str, int)`, indistinguishable from `TCPAddress`, and iroh's
is a `(str, str)`, which the *existing* UDS case
(`case (_, filename) if type(filename) is str`) already swallows.
So the contract doc (§1.1) now carries the conclusion as a
**recommended prerequisite for all three backends**: make the
unwrapped form carry an explicit proto-key spelled with the
`multiaddr` protocol name — `('tcp', host, port)`,
`('unix', path)`, `('tipc', stype, inst, scope)`. `wrap_address()`
then collapses from an order-sensitive `match` to
`_address_types[addr[0]]` and the whole collision class stops
existing, while the on-wire form finally agrees w/
`mk_maddr()`/`parse_maddr()` instead of being an independent
invention.
Two consequences spelled out: it's a wire-format change
(`SpawnSpec`, `_root_mailbox`, `_registry_addrs`) + every fixture
+ downstream config, so it wants its own migration commit landed
*before* any new backend; and it's the moment to stop handing raw
tuples to users at all — `Address` becomes the public currency
and `UnwrappedAddress` an internal serialization detail, the same
discipline `ipaddress` uses (you pass `IPv4Address`, never a
4-tuple).
Plan 01 §2.2 is rewritten to match and to explicitly **retract**
its own earlier `('tipc:<stype>:<scope>', instance)` self-tagging
prefix hack — it keeps `wrap_address()` order-sensitive and does
nothing for the iroh/UDS collision, so the doc says don't
resurrect it. Registration checklist item 4 likewise becomes "do
the migration first, then this is a one-line `_address_types`
entry".
Also seeds a `/tipc` multiaddr-spec submission as a follow-up,
mirroring the `wg` track (multiformats/py-multiaddr#107/#108 + gh
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Landing page for `ai/tpt-backends/`: points at the contract spec
as required first reading, tables the 3 plans against their
issues/deps/size, and states the landing order + why.
Deats,
- TIPC first as the cheap proof the table-registration story
generalizes to a genuinely new proto (stdlib-only, and
`trio`'s sock wrappers are family-agnostic).
- `wg` layer-A next since it's deployable-today doc/example work.
- QUIC last, gated on its own prep PR.
- notes that plans 01 and 02 both want the same
`Address.rebind_from_sockname` gate, so whichever lands first
ships it.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #482 + the tunnelled-maddr item of #443. Pushes
back on the framing that `wg` is a tpt: it's transparent to
`socket(2)`, so it belongs as a *bindspace* — a scoped
`@acm`-managed net ctx that an existing L4 tpt binds *inside* —
and it's what finally implements the long-spec'd (never
implemented) `Address.namespace`.
Deats, 3 independently-shippable layers,
- A) declarative: commit #482's examples, teach `parse_maddr()`
the `/…/wg/u<key>` suffix -> a `TunnelledAddress` wrapper whose
`.proto_key`/`.unwrap()` delegate to `.inner` so nothing new
crosses the wire and every existing table lookup keeps working.
- B) swap the `subprocess.run(['sudo', 'wg', 'show'])` shelling
for `pyroute2`. Default to `trio.to_thread` around the sync API
(these are one-shot ops at bind/teardown, never hot-path), w/
sans-io codecs + a trio `AF_NETLINK` sock as the follow-up for
the read paths. Explicitly forbids dragging `trio-asyncio` in.
- C) `open_bindspace()`/`open_netns()`/`open_wg_iface()` `@acm`s
folded w/ an `AsyncExitStack`, + filling in the
`# !TODO, always be ns aware!` placeholder already sitting in
`Endpoint.pformat()`.
Also flags the subtlest bug in the whole thing: `setns(2)` is
*per-thread*, so a `pyroute2` query issued via `trio.to_thread`
lands in the *original* netns. Test-first, per usual.
Further, designs for the generalization (`TunnelSpec` union +
`match` dispatch) while only implementing `wg`+netns, and calls
out `veth`-in-netns as the better *first* one bc it makes a
fully self-contained two-"host" integration test possible w/o
`wg` at all.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #353. Picks `iroh` (the `uniffi` FFI pkg) over
`aioquic`/`quiche` bc node-id addressing + hole-punching + relay
fallback is the whole point; `aioquic` stays documented as the
fallback since ~90% of the adapters here are reusable against a
sans-io core.
Deats,
- the layering: iroh `Endpoint` per actor, `Connection` per peer
(pooled via `trionics.maybe_open_context()`, not a hand-rolled
cache), one bi-stream per `Channel`. 4-byte prefix framing
stays so `MsgpackTransport` is untouched.
- `_uniffi_trio.py`: uniffi only uses `asyncio` as the executor
for its rust-future poll loop, so a ~40-line
`TrioToken.run_sync_soon()` bridge replaces it. Spells out the
real hazards — strong ref on the `ctypes` trampoline, poll-code
propagation, and a *bounded* shielded cancel-drain so a wedged
rust future can't make an actor un-cancellable.
- `IrohAddress` w/ ALPN as the `.bindspace`, the `(str, str)`
unwrapped form's collision w/ the UDS match-case, and why
`get_root()` needs a persisted secret key -> a lazy
`default_lo_addrs()` + a pure-getter/explicit-setter split.
- `QuicMsgStream(trio.abc.HalfCloseableStream)` +
`QuicListener(trio.abc.Listener)`, incl. the exact
EOF/reset/use-after-close semantics `_transport.py` already
match-cases on, and hanging the acceptor tasks off the
existing `Endpoint.listen_tn`.
- a prep-PR boundary: annotation widening, the shared
`rebind_from_sockname` gate and a `tpt_key`-based
`transport_from_stream()` dispatch, all landable w/ tcp/uds as
the only backends.
Further, notes this is our first tpt w/ real transport security
+ peer auth, so an inbound node-id allowlist hook belongs here —
and that it says nothing about the other backends.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Plan doc for gh #378, the cheapest new backend we can add: it's
stdlib-only (CPython ships `AF_TIPC` + 23 `TIPC_*` consts) and
per the contract doc `trio`'s stream/listener wrappers don't care
about the addr family, so `MsgpackTransport` framing and
`trio.serve_listeners()` are reused verbatim.
Deats,
- `TIPCAddress` as a *service name* `(type, instance)` w/ scope
as the `.bindspace`; `bind()` publishes the singleton
name-range, peers `connect()` by name and the kernel resolves
+ load-balances. I.e. registration/lookup for free, no
registrar in the loop.
- the self-tagging `('tipc:<stype>:<scope>', instance)` unwrapped
form + why it must be match-ordered before `TCPAddress`'s.
- `get_random()` via a blake2b digest of the actor id (there's no
`port=0` analogue) and the silent-crosstalk risk that follows:
TIPC *allows* dup binders and round-robins, so a collision
doesn't `EADDRINUSE`, it cross-talks.
- an `Address.rebind_from_sockname` ClassVar to opt out of
`Endpoint.start_listener()`'s `getsockname()` reconcile, which
for TIPC always returns a port-id, never the bound name.
- the `TIPC_TOP_SRV` topology-service subscription as an `@acm`
yielding a chan of typed name-table events — push-based
register/dereg, the real "end game cluster proto" bit.
- commit sequencing, hard capability gating (`modprobe tipc`;
bare `AF_TIPC` is `EAFNOSUPPORT` on a stock box), CI matrix
notes, risks + follow-up seeds.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
First doc of a new `ai/tpt-backends/` set: the normative
description of what a `tractor` tpt backend *is* as of `main`,
written so the 3 sibling plans (TIPC, QUIC, `wg`) can be worked
independently (by another model/provider) w/o design drift.
Deats,
- the backend duck-type as empirically derived from
`_tcp.py`/`_uds.py`: the `Address` protocol surface, the
mod-level `start_listener()`/`close_listener()` pair and
`Msgpack<Proto>Stream(MsgpackTransport)`.
- the ONE reflection you can't break:
`Endpoint.start_listener()` resolves the tpt mod via
`inspect.getmodule(self.addr)`, so an `Address` type and its
listener fns MUST live in the same mod.
- a 10-item registration checklist (`_address_types`,
`_key_to_transport`, `_addr_to_transport`, `wrap_address()`
match-cases, `TransportProtocolKey`, maddr tables, ..) incl.
the import-time `_default_lo_addrs` trap.
- where the `trio.SocketListener` assumption is *actually*
load-bearing (just the `getsockname()` reconcile) vs. merely
annotated.
- the handshake/discovery invariants a new backend inherits,
dep policy (extras + import-laziness per the #470 boot-latency
budget), `--tpt-proto` harness plumbing and code style.
Also, records a verified finding the plans lean on hard:
`trio.SocketStream`/`SocketListener` are addr-*family* agnostic
— the only ctor checks are "is a trio sock" + `SOCK_STREAM` (+
an `OSError`-suppressed `SO_ACCEPTCONN`) — so any `SOCK_STREAM`
family CPython can make drops into the existing
`trio.serve_listeners()` path unmodified.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
Match `Actor.accept_addr`'s declared `tuple[str, int|str]` contract
instead of narrowing its second item to the UDS-specific string path.
Review: https://github.com/goodboy/tractor/pull/512#discussion_r3888153533
(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))