Compare commits
No commits in common. "wkt/pr510_review" and "main" have entirely different histories.
wkt/pr510_
...
main
|
|
@ -1,38 +0,0 @@
|
||||||
# Docs TODOs
|
|
||||||
|
|
||||||
## Auto-sync README code examples with source
|
|
||||||
|
|
||||||
The `docs/README.rst` has inline code blocks that
|
|
||||||
duplicate actual example files (e.g.
|
|
||||||
`examples/infected_asyncio_echo_server.py`). Every time
|
|
||||||
the public API changes we have to manually sync both.
|
|
||||||
|
|
||||||
Sphinx's `literalinclude` directive can pull code directly
|
|
||||||
from source files:
|
|
||||||
|
|
||||||
```rst
|
|
||||||
.. literalinclude:: ../examples/infected_asyncio_echo_server.py
|
|
||||||
:language: python
|
|
||||||
:caption: examples/infected_asyncio_echo_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Or to include only a specific function/section:
|
|
||||||
|
|
||||||
```rst
|
|
||||||
.. literalinclude:: ../examples/infected_asyncio_echo_server.py
|
|
||||||
:language: python
|
|
||||||
:pyobject: aio_echo_server
|
|
||||||
```
|
|
||||||
|
|
||||||
This way the docs always reflect the actual code without
|
|
||||||
manual syncing.
|
|
||||||
|
|
||||||
### Considerations
|
|
||||||
- `README.rst` is also rendered on GitHub/PyPI which do
|
|
||||||
NOT support `literalinclude` - so we'd need a build
|
|
||||||
step or a separate `_sphinx_readme.rst` (which already
|
|
||||||
exists at `docs/github_readme/_sphinx_readme.rst`).
|
|
||||||
- Could use a pre-commit hook or CI step to extract code
|
|
||||||
from examples into the README for GitHub rendering.
|
|
||||||
- Another option: `sphinx-autodoc` style approach where
|
|
||||||
docstrings from the actual module are pulled in.
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
# `RuntimeVars` env-var lift — design plan
|
|
||||||
|
|
||||||
Status: **draft, awaiting user edits**
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Consolidate the sprawl of pytest CLI flags + ad-hoc env vars +
|
|
||||||
hardcoded fixture defaults into a *single* env-var-encoded
|
|
||||||
runtime-vars envelope, with a typed in-memory representation
|
|
||||||
(`tractor.runtime._state.RuntimeVars`) as the sole source of
|
|
||||||
truth.
|
|
||||||
|
|
||||||
## Why now
|
|
||||||
|
|
||||||
- `--tpt-proto`, `--spawn-backend`, `--diag-on-hang`,
|
|
||||||
`--diag-capture-delay` and (soon) `TRACTOR_REG_ADDR` etc. are
|
|
||||||
proliferating. Each adds a parsing seam.
|
|
||||||
- `tests/devx/test_debugger.py` invokes example scripts as
|
|
||||||
separate subprocesses; they currently can't see the
|
|
||||||
fixture-allocated `reg_addr` at all (root cause of why
|
|
||||||
parametrizing devx scripts on `reg_addr` is on your TODO).
|
|
||||||
- Concurrent pytest sessions on the same host collide on
|
|
||||||
shared defaults (the `registry@1616` race we just fixed is
|
|
||||||
one symptom; per-session unique addr is the structural
|
|
||||||
fix).
|
|
||||||
- `tractor.runtime._state.RuntimeVars: Struct` is already
|
|
||||||
defined and **unused** — its docstring even says it
|
|
||||||
"should be utilized as possible for future calls."
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
### Module: `tractor/_testing/_rtvars.py`
|
|
||||||
|
|
||||||
Lifted from `modden.runtime.env`, ~50 LOC, no new deps.
|
|
||||||
|
|
||||||
```python
|
|
||||||
_TRACTOR_RT_VARS_OSENV: str = '_TRACTOR_RT_VARS'
|
|
||||||
|
|
||||||
def dump_rtvars(rtvars: RuntimeVars|dict) -> tuple[str, str]:
|
|
||||||
'''str-serialize via `str(dict)` — ast.literal_eval-able'''
|
|
||||||
|
|
||||||
def load_rtvars(env: dict) -> RuntimeVars:
|
|
||||||
'''ast.literal_eval the env-var value, hydrate to struct'''
|
|
||||||
|
|
||||||
def get_rtvars(proc: psutil.Process|None = None) -> RuntimeVars:
|
|
||||||
'''read the var from a target proc's env (or current)'''
|
|
||||||
|
|
||||||
def update_rtvars(
|
|
||||||
rtvars: RuntimeVars|dict|None = None,
|
|
||||||
update_osenv: bool|dict = True,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
'''mutate + re-encode + (optionally) write to os.environ'''
|
|
||||||
```
|
|
||||||
|
|
||||||
### Encoding choice: `str(dict)` + `ast.literal_eval`
|
|
||||||
|
|
||||||
Pros:
|
|
||||||
- stdlib only
|
|
||||||
- handles all the types tractor's tests need: `str`, `int`,
|
|
||||||
`float`, `bool`, `None`, `list`, `tuple`, `dict`
|
|
||||||
- human-readable in the env (greppable, inspectable via
|
|
||||||
`cat /proc/<pid>/environ | tr '\0' '\n'`)
|
|
||||||
|
|
||||||
Cons:
|
|
||||||
- non-stdlib types (msgspec Structs, `Path`, custom classes)
|
|
||||||
must be lowered first — fine for the test fixture set
|
|
||||||
- not stable across Python versions for esoteric repr cases
|
|
||||||
(we don't hit any)
|
|
||||||
|
|
||||||
Alternatives considered:
|
|
||||||
- **msgpack**: adds a dep + binary form is ungreppable
|
|
||||||
- **json**: doesn't preserve tuples (becomes lists), which is
|
|
||||||
a common type for `reg_addr`
|
|
||||||
- **toml/yaml**: heavier deps, no real benefit
|
|
||||||
|
|
||||||
### `RuntimeVars` becomes the single source of truth
|
|
||||||
|
|
||||||
The legacy `_runtime_vars: dict[str, Any]` global in
|
|
||||||
`runtime/_state.py` becomes a *cached view* of a
|
|
||||||
`RuntimeVars` singleton instance:
|
|
||||||
|
|
||||||
- `get_runtime_vars()` returns either the struct or a
|
|
||||||
`.to_dict()` view depending on caller's preference
|
|
||||||
- `set_runtime_vars(...)` validates against the struct schema
|
|
||||||
- spawn-time SpawnSpec sends the struct (already does
|
|
||||||
conceptually — just gets typed)
|
|
||||||
- `__setattr__` `breakpoint()` debug instrumentation gets
|
|
||||||
removed (unrelated cleanup, mentioned in conversation)
|
|
||||||
|
|
||||||
### Migration path
|
|
||||||
|
|
||||||
**Phase 0** *(prep)*: strip the stray `breakpoint()` from
|
|
||||||
`RuntimeVars.__setattr__`.
|
|
||||||
|
|
||||||
**Phase 1**: land `_rtvars.py` as a leaf module, used only by
|
|
||||||
test infra. Subprocess-spawned scripts in `tests/devx/`
|
|
||||||
read `_TRACTOR_RT_VARS` on startup → reconstruct
|
|
||||||
`RuntimeVars` → call `tractor.open_root_actor(**rtvars.as_kwargs())`.
|
|
||||||
Concurrent runs become deterministic-isolated because each
|
|
||||||
session writes a unique `_registry_addrs` into the env.
|
|
||||||
|
|
||||||
**Phase 2**: migrate runtime callers (`_state.get_runtime_vars`,
|
|
||||||
spawn `SpawnSpec`, `Actor.async_main`) to operate on the
|
|
||||||
struct directly, with the dict as a compat view that gets
|
|
||||||
deprecated.
|
|
||||||
|
|
||||||
**Phase 3** *(structural)*: per-session bindspace subdir
|
|
||||||
`/run/user/<uid>/tractor/<session_uuid>/` — encoded in the
|
|
||||||
rt-vars envelope, picked up by every subactor automatically.
|
|
||||||
Obsoletes the entire bindspace-leak warning class.
|
|
||||||
|
|
||||||
## Open design questions (user input wanted)
|
|
||||||
|
|
||||||
- (placeholder for your edits)
|
|
||||||
- (placeholder)
|
|
||||||
- (placeholder)
|
|
||||||
|
|
||||||
## Out-of-scope for this lift
|
|
||||||
|
|
||||||
- Anything in `modden.runtime.env` related to `Spawn`,
|
|
||||||
`WmCtl`, `Wks` — that's a workspace orchestration layer,
|
|
||||||
not an env-var helper. We only lift the four utility
|
|
||||||
functions + the var name constant.
|
|
||||||
- Switching to msgpack/json — explicitly chosen against
|
|
||||||
above.
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(cp .claude/*)",
|
|
||||||
"Read(.claude/**)",
|
|
||||||
"Read(.claude/skills/run-tests/**)",
|
|
||||||
"Write(.claude/**/*commit_msg*)",
|
|
||||||
"Write(.claude/git_commit_msg_LATEST.md)",
|
|
||||||
"Skill(run-tests)",
|
|
||||||
"Skill(close-wkt)",
|
|
||||||
"Skill(open-wkt)",
|
|
||||||
"Skill(prompt-io)",
|
|
||||||
"Bash(date *)",
|
|
||||||
"Bash(git diff *)",
|
|
||||||
"Bash(git log *)",
|
|
||||||
"Bash(git status)",
|
|
||||||
"Bash(git remote:*)",
|
|
||||||
"Bash(git stash:*)",
|
|
||||||
"Bash(git mv:*)",
|
|
||||||
"Bash(git rev-parse:*)",
|
|
||||||
"Bash(test:*)",
|
|
||||||
"Bash(ls:*)",
|
|
||||||
"Bash(grep:*)",
|
|
||||||
"Bash(find:*)",
|
|
||||||
"Bash(ln:*)",
|
|
||||||
"Bash(cat:*)",
|
|
||||||
"Bash(mkdir:*)",
|
|
||||||
"Bash(gh pr:*)",
|
|
||||||
"Bash(gh api:*)",
|
|
||||||
"Bash(gh issue:*)",
|
|
||||||
"Bash(UV_PROJECT_ENVIRONMENT=py* uv sync:*)",
|
|
||||||
"Bash(UV_PROJECT_ENVIRONMENT=py* uv run:*)",
|
|
||||||
"Bash(echo EXIT:$?:*)",
|
|
||||||
"Bash(echo \"EXIT=$?\")",
|
|
||||||
"Read(/tmp/**)"
|
|
||||||
],
|
|
||||||
"deny": [],
|
|
||||||
"ask": []
|
|
||||||
},
|
|
||||||
"prefersReducedMotion": false,
|
|
||||||
"outputStyle": "default"
|
|
||||||
}
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
# Commit Message Style Guide for `tractor`
|
|
||||||
|
|
||||||
Analysis based on 500 recent commits from the `tractor` repository.
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
Write commit messages that are technically precise yet casual in
|
|
||||||
tone. Use abbreviations and informal language while maintaining
|
|
||||||
clarity about what changed and why.
|
|
||||||
|
|
||||||
## Subject Line Format
|
|
||||||
|
|
||||||
### Length and Structure
|
|
||||||
- Target: ~50 chars with a hard-max of 67.
|
|
||||||
- Use backticks around code elements (72.2% of commits)
|
|
||||||
- Rarely use colons (5.2%), except for file prefixes
|
|
||||||
- End with '?' for uncertain changes (rare: 0.8%)
|
|
||||||
- End with '!' for important changes (rare: 2.0%)
|
|
||||||
|
|
||||||
### Opening Verbs (Present Tense)
|
|
||||||
|
|
||||||
Most common verbs from analysis:
|
|
||||||
- `Add` (14.4%) - wholly new features/functionality
|
|
||||||
- `Use` (4.4%) - adopt new approach/tool
|
|
||||||
- `Drop` (3.6%) - remove code/feature
|
|
||||||
- `Fix` (2.4%) - bug fixes
|
|
||||||
- `Move`/`Mv` (3.6%) - relocate code
|
|
||||||
- `Adjust` (2.0%) - minor tweaks
|
|
||||||
- `Update` (1.6%) - enhance existing feature
|
|
||||||
- `Bump` (1.2%) - dependency updates
|
|
||||||
- `Rename` (1.2%) - identifier changes
|
|
||||||
- `Set` (1.2%) - configuration changes
|
|
||||||
- `Handle` (1.0%) - add handling logic
|
|
||||||
- `Raise` (1.0%) - add error raising
|
|
||||||
- `Pass` (0.8%) - pass parameters/values
|
|
||||||
- `Support` (0.8%) - add support for something
|
|
||||||
- `Hide` (1.4%) - make private/internal
|
|
||||||
- `Always` (1.4%) - enforce consistent behavior
|
|
||||||
- `Mk` (1.4%) - make/create (abbreviated)
|
|
||||||
- `Start` (1.0%) - begin implementation
|
|
||||||
|
|
||||||
Other frequent verbs: `More`, `Change`, `Extend`, `Disable`, `Log`,
|
|
||||||
`Enable`, `Ensure`, `Expose`, `Allow`
|
|
||||||
|
|
||||||
### Backtick Usage
|
|
||||||
|
|
||||||
Always use backticks for:
|
|
||||||
- Module names: `trio`, `asyncio`, `msgspec`, `greenback`, `stackscope`
|
|
||||||
- Class names: `Context`, `Actor`, `Address`, `PldRx`, `SpawnSpec`
|
|
||||||
- Method names: `.pause_from_sync()`, `._pause()`, `.cancel()`
|
|
||||||
- Function names: `breakpoint()`, `collapse_eg()`, `open_root_actor()`
|
|
||||||
- Decorators: `@acm`, `@context`
|
|
||||||
- Exceptions: `Cancelled`, `TransportClosed`, `MsgTypeError`
|
|
||||||
- Keywords: `finally`, `None`, `False`
|
|
||||||
- Variable names: `tn`, `debug_mode`
|
|
||||||
- Complex expressions: `trio.Cancelled`, `asyncio.Task`
|
|
||||||
|
|
||||||
Most backticked terms in tractor:
|
|
||||||
`trio`, `asyncio`, `Context`, `.pause_from_sync()`, `tn`,
|
|
||||||
`._pause()`, `breakpoint()`, `collapse_eg()`, `Actor`, `@acm`,
|
|
||||||
`.cancel()`, `Cancelled`, `open_root_actor()`, `greenback`
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
Good subject lines:
|
|
||||||
```
|
|
||||||
Add `uds` to `._multiaddr`, tweak typing
|
|
||||||
Drop `DebugStatus.shield` attr, add `.req_finished`
|
|
||||||
Use `stackscope` for all actor-tree rendered "views"
|
|
||||||
Fix `.to_asyncio` inter-task-cancellation!
|
|
||||||
Bump `ruff.toml` to target py313
|
|
||||||
Mv `load_module_from_path()` to new `._code_load` submod
|
|
||||||
Always use `tuple`-cast for singleton parent addrs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Body Format
|
|
||||||
|
|
||||||
### General Structure
|
|
||||||
- 43.2% of commits have no body (simple changes)
|
|
||||||
- Use blank line after subject
|
|
||||||
- Max line length: 67 chars
|
|
||||||
- Use `-` bullets for lists (28.0% of commits)
|
|
||||||
- Rarely use `*` bullets (2.4%)
|
|
||||||
|
|
||||||
### Section Markers
|
|
||||||
|
|
||||||
Use these markers to organize longer commit bodies:
|
|
||||||
- `Also,` (most common: 26 occurrences)
|
|
||||||
- `Other,` (13 occurrences)
|
|
||||||
- `Deats,` (11 occurrences) - for implementation details
|
|
||||||
- `Further,` (7 occurrences)
|
|
||||||
- `TODO,` (3 occurrences)
|
|
||||||
- `Impl details,` (2 occurrences)
|
|
||||||
- `Notes,` (1 occurrence)
|
|
||||||
|
|
||||||
### Common Abbreviations
|
|
||||||
|
|
||||||
Use these freely (sorted by frequency):
|
|
||||||
- `msg` (63) - message
|
|
||||||
- `bg` (37) - background
|
|
||||||
- `ctx` (30) - context
|
|
||||||
- `impl` (27) - implementation
|
|
||||||
- `mod` (26) - module
|
|
||||||
- `obvi` (17) - obviously
|
|
||||||
- `tn` (16) - task name
|
|
||||||
- `fn` (15) - function
|
|
||||||
- `vs` (15) - versus
|
|
||||||
- `bc` (14) - because
|
|
||||||
- `var` (14) - variable
|
|
||||||
- `prolly` (9) - probably
|
|
||||||
- `ep` (6) - entry point
|
|
||||||
- `OW` (5) - otherwise
|
|
||||||
- `rn` (4) - right now
|
|
||||||
- `sig` (4) - signal/signature
|
|
||||||
- `deps` (3) - dependencies
|
|
||||||
- `iface` (2) - interface
|
|
||||||
- `subproc` (2) - subprocess
|
|
||||||
- `tho` (2) - though
|
|
||||||
- `ofc` (2) - of course
|
|
||||||
|
|
||||||
### Tone and Style
|
|
||||||
|
|
||||||
- Casual but technical (use `XD` for humor: 23 times)
|
|
||||||
- Use `..` for trailing thoughts (108 occurrences)
|
|
||||||
- Use `Woops,` to acknowledge mistakes (4 subject lines)
|
|
||||||
- Don't be afraid to show personality while being precise
|
|
||||||
|
|
||||||
### Example Bodies
|
|
||||||
|
|
||||||
Simple with bullets:
|
|
||||||
```
|
|
||||||
Add `multiaddr` and bump up some deps
|
|
||||||
|
|
||||||
Since we're planning to use it for (discovery)
|
|
||||||
addressing, allowing replacement of the hacky (pretend)
|
|
||||||
attempt in `tractor._multiaddr` Bp
|
|
||||||
|
|
||||||
Also pin some deps,
|
|
||||||
- make us py312+
|
|
||||||
- use `pdbp` with my frame indexing fix.
|
|
||||||
- mv to latest `xonsh` for fancy cmd/suggestion injections.
|
|
||||||
|
|
||||||
Bump lock file to match obvi!
|
|
||||||
```
|
|
||||||
|
|
||||||
With section markers:
|
|
||||||
```
|
|
||||||
Use `stackscope` for all actor-tree rendered "views"
|
|
||||||
|
|
||||||
Instead of the (much more) limited and hacky `.devx._code`
|
|
||||||
impls, move to using the new `.devx._stackscope` API which
|
|
||||||
wraps the `stackscope` project.
|
|
||||||
|
|
||||||
Deats,
|
|
||||||
- make new `stackscope.extract_stack()` wrapper
|
|
||||||
- port over frame-descing to `_stackscope.pformat_stack()`
|
|
||||||
- move `PdbREPL` to use `stackscope` render approach
|
|
||||||
- update tests for new stack output format
|
|
||||||
|
|
||||||
Also,
|
|
||||||
- tweak log formatting for consistency
|
|
||||||
- add typing hints throughout
|
|
||||||
```
|
|
||||||
|
|
||||||
## Special Patterns
|
|
||||||
|
|
||||||
### WIP Commits
|
|
||||||
Rare (0.2%) - avoid committing WIP if possible
|
|
||||||
|
|
||||||
### Merge Commits
|
|
||||||
Auto-generated (4.4%), don't worry about style
|
|
||||||
|
|
||||||
### File References
|
|
||||||
- Use `module.py` or `.submodule` style
|
|
||||||
- Rarely use `file.py:line` references (0 in analysis)
|
|
||||||
|
|
||||||
### Links
|
|
||||||
- GitHub links used sparingly (3 total)
|
|
||||||
- Prefer code references over external links
|
|
||||||
|
|
||||||
## Footer
|
|
||||||
|
|
||||||
The default footer should credit `claude` (you) for helping generate
|
|
||||||
the commit msg content:
|
|
||||||
|
|
||||||
```
|
|
||||||
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
Further, if the patch was solely or in part written
|
|
||||||
by `claude`, instead add:
|
|
||||||
|
|
||||||
```
|
|
||||||
(this patch was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary Checklist
|
|
||||||
|
|
||||||
Before committing, verify:
|
|
||||||
- [ ] Subject line uses present tense verb
|
|
||||||
- [ ] Subject line ~50 chars (hard max 67)
|
|
||||||
- [ ] Code elements wrapped in backticks
|
|
||||||
- [ ] Body lines ≤67 chars
|
|
||||||
- [ ] Abbreviations used where natural
|
|
||||||
- [ ] Casual yet precise tone
|
|
||||||
- [ ] Section markers if body >3 paragraphs
|
|
||||||
- [ ] Technical accuracy maintained
|
|
||||||
|
|
||||||
## Analysis Metadata
|
|
||||||
|
|
||||||
```
|
|
||||||
Source: tractor repository
|
|
||||||
Commits analyzed: 500
|
|
||||||
Date range: 2019-2025
|
|
||||||
Analysis date: 2026-02-08
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
(this style guide was generated by [`claude-code`][claude-code-gh]
|
|
||||||
analyzing commit history)
|
|
||||||
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
---
|
|
||||||
name: conc-anal
|
|
||||||
description: >
|
|
||||||
Concurrency analysis for tractor's trio-based
|
|
||||||
async primitives. Trace task scheduling across
|
|
||||||
checkpoint boundaries, identify race windows in
|
|
||||||
shared mutable state, and verify synchronization
|
|
||||||
correctness. Invoke on code segments the user
|
|
||||||
points at, OR proactively when reviewing/writing
|
|
||||||
concurrent cache, lock, or multi-task acm code.
|
|
||||||
argument-hint: "[file:line-range or function name]"
|
|
||||||
allowed-tools:
|
|
||||||
- Read
|
|
||||||
- Grep
|
|
||||||
- Glob
|
|
||||||
- Task
|
|
||||||
---
|
|
||||||
|
|
||||||
Perform a structured concurrency analysis on the
|
|
||||||
target code. This skill should be invoked:
|
|
||||||
|
|
||||||
- **On demand**: user points at a code segment
|
|
||||||
(file:lines, function name, or pastes a snippet)
|
|
||||||
- **Proactively**: when writing or reviewing code
|
|
||||||
that touches shared mutable state across trio
|
|
||||||
tasks — especially `_Cache`, locks, events, or
|
|
||||||
multi-task `@acm` lifecycle management
|
|
||||||
|
|
||||||
## 0. Identify the target
|
|
||||||
|
|
||||||
If the user provides a file:line-range or function
|
|
||||||
name, read that code. If not explicitly provided,
|
|
||||||
identify the relevant concurrent code from context
|
|
||||||
(e.g. the current diff, a failing test, or the
|
|
||||||
function under discussion).
|
|
||||||
|
|
||||||
## 1. Inventory shared mutable state
|
|
||||||
|
|
||||||
List every piece of state that is accessed by
|
|
||||||
multiple tasks. For each, note:
|
|
||||||
|
|
||||||
- **What**: the variable/dict/attr (e.g.
|
|
||||||
`_Cache.values`, `_Cache.resources`,
|
|
||||||
`_Cache.users`)
|
|
||||||
- **Scope**: class-level, module-level, or
|
|
||||||
closure-captured
|
|
||||||
- **Writers**: which tasks/code-paths mutate it
|
|
||||||
- **Readers**: which tasks/code-paths read it
|
|
||||||
- **Guarded by**: which lock/event/ordering
|
|
||||||
protects it (or "UNGUARDED" if none)
|
|
||||||
|
|
||||||
Format as a table:
|
|
||||||
|
|
||||||
```
|
|
||||||
| State | Writers | Readers | Guard |
|
|
||||||
|---------------------|-----------------|-----------------|----------------|
|
|
||||||
| _Cache.values | run_ctx, moc¹ | moc | ctx_key lock |
|
|
||||||
| _Cache.resources | run_ctx, moc | moc, run_ctx | UNGUARDED |
|
|
||||||
```
|
|
||||||
|
|
||||||
¹ `moc` = `maybe_open_context`
|
|
||||||
|
|
||||||
## 2. Map checkpoint boundaries
|
|
||||||
|
|
||||||
For each code path through the target, mark every
|
|
||||||
**checkpoint** — any `await` expression where trio
|
|
||||||
can switch to another task. Use line numbers:
|
|
||||||
|
|
||||||
```
|
|
||||||
L325: await lock.acquire() ← CHECKPOINT
|
|
||||||
L395: await service_tn.start(...) ← CHECKPOINT
|
|
||||||
L411: lock.release() ← (not a checkpoint, but changes lock state)
|
|
||||||
L414: yield (False, yielded) ← SUSPEND (caller runs)
|
|
||||||
L485: no_more_users.set() ← (wakes run_ctx, no switch yet)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key trio scheduling rules to apply:**
|
|
||||||
- `Event.set()` makes waiters *ready* but does NOT
|
|
||||||
switch immediately
|
|
||||||
- `lock.release()` is not a checkpoint
|
|
||||||
- `await sleep(0)` IS a checkpoint
|
|
||||||
- Code in `finally` blocks CAN have checkpoints
|
|
||||||
(unlike asyncio)
|
|
||||||
- `await` inside `except` blocks can be
|
|
||||||
`trio.Cancelled`-masked
|
|
||||||
|
|
||||||
## 3. Trace concurrent task schedules
|
|
||||||
|
|
||||||
Write out the **interleaved execution trace** for
|
|
||||||
the problematic scenario. Number each step and tag
|
|
||||||
which task executes it:
|
|
||||||
|
|
||||||
```
|
|
||||||
[Task A] 1. acquires lock
|
|
||||||
[Task A] 2. cache miss → allocates resources
|
|
||||||
[Task A] 3. releases lock
|
|
||||||
[Task A] 4. yields to caller
|
|
||||||
[Task A] 5. caller exits → finally runs
|
|
||||||
[Task A] 6. users-- → 0, sets no_more_users
|
|
||||||
[Task A] 7. pops lock from _Cache.locks
|
|
||||||
[run_ctx] 8. wakes from no_more_users.wait()
|
|
||||||
[run_ctx] 9. values.pop(ctx_key)
|
|
||||||
[run_ctx] 10. acm __aexit__ → CHECKPOINT
|
|
||||||
[Task B] 11. creates NEW lock (old one popped)
|
|
||||||
[Task B] 12. acquires immediately
|
|
||||||
[Task B] 13. values[ctx_key] → KeyError
|
|
||||||
[Task B] 14. resources[ctx_key] → STILL EXISTS
|
|
||||||
[Task B] 15. 💥 RuntimeError
|
|
||||||
```
|
|
||||||
|
|
||||||
Identify the **race window**: the range of steps
|
|
||||||
where state is inconsistent. In the example above,
|
|
||||||
steps 9–10 are the window (values gone, resources
|
|
||||||
still alive).
|
|
||||||
|
|
||||||
## 4. Classify the bug
|
|
||||||
|
|
||||||
Categorize what kind of concurrency issue this is:
|
|
||||||
|
|
||||||
- **TOCTOU** (time-of-check-to-time-of-use): state
|
|
||||||
changes between a check and the action based on it
|
|
||||||
- **Stale reference**: a task holds a reference to
|
|
||||||
state that another task has invalidated
|
|
||||||
- **Lifetime mismatch**: a synchronization primitive
|
|
||||||
(lock, event) has a shorter lifetime than the
|
|
||||||
state it's supposed to protect
|
|
||||||
- **Missing guard**: shared state is accessed
|
|
||||||
without any synchronization
|
|
||||||
- **Atomicity gap**: two operations that should be
|
|
||||||
atomic have a checkpoint between them
|
|
||||||
|
|
||||||
## 5. Propose fixes
|
|
||||||
|
|
||||||
For each proposed fix, provide:
|
|
||||||
|
|
||||||
- **Sketch**: pseudocode or diff showing the change
|
|
||||||
- **How it closes the window**: which step(s) from
|
|
||||||
the trace it eliminates or reorders
|
|
||||||
- **Tradeoffs**: complexity, perf, new edge cases,
|
|
||||||
impact on other code paths
|
|
||||||
- **Risk**: what could go wrong (deadlocks, new
|
|
||||||
races, cancellation issues)
|
|
||||||
|
|
||||||
Rate each fix: `[simple|moderate|complex]` impl
|
|
||||||
effort.
|
|
||||||
|
|
||||||
## 6. Output format
|
|
||||||
|
|
||||||
Structure the full analysis as:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Concurrency analysis: `<target>`
|
|
||||||
|
|
||||||
### Shared state
|
|
||||||
<table from step 1>
|
|
||||||
|
|
||||||
### Checkpoints
|
|
||||||
<list from step 2>
|
|
||||||
|
|
||||||
### Race trace
|
|
||||||
<interleaved trace from step 3>
|
|
||||||
|
|
||||||
### Classification
|
|
||||||
<bug type from step 4>
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
<proposals from step 5>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tractor-specific patterns to watch
|
|
||||||
|
|
||||||
These are known problem areas in tractor's
|
|
||||||
concurrency model. Flag them when encountered:
|
|
||||||
|
|
||||||
### `_Cache` lock vs `run_ctx` lifetime
|
|
||||||
|
|
||||||
The `_Cache.locks` entry is managed by
|
|
||||||
`maybe_open_context` callers, but `run_ctx` runs
|
|
||||||
in `service_tn` — a different task tree. Lock
|
|
||||||
pop/release in the caller's `finally` does NOT
|
|
||||||
wait for `run_ctx` to finish tearing down. Any
|
|
||||||
state that `run_ctx` cleans up in its `finally`
|
|
||||||
(e.g. `resources.pop()`) is vulnerable to
|
|
||||||
re-entry races after the lock is popped.
|
|
||||||
|
|
||||||
### `values.pop()` → acm `__aexit__` → `resources.pop()` gap
|
|
||||||
|
|
||||||
In `_Cache.run_ctx`, the inner `finally` pops
|
|
||||||
`values`, then the acm's `__aexit__` runs (which
|
|
||||||
has checkpoints), then the outer `finally` pops
|
|
||||||
`resources`. This creates a window where `values`
|
|
||||||
is gone but `resources` still exists — a classic
|
|
||||||
atomicity gap.
|
|
||||||
|
|
||||||
### Global vs per-key counters
|
|
||||||
|
|
||||||
`_Cache.users` as a single `int` (pre-fix) meant
|
|
||||||
that users of different `ctx_key`s inflated each
|
|
||||||
other's counts, preventing teardown when one key's
|
|
||||||
users hit zero. Always verify that per-key state
|
|
||||||
(`users`, `locks`) is actually keyed on `ctx_key`
|
|
||||||
and not on `fid` or some broader key.
|
|
||||||
|
|
||||||
### `Event.set()` wakes but doesn't switch
|
|
||||||
|
|
||||||
`trio.Event.set()` makes waiting tasks *ready* but
|
|
||||||
the current task continues executing until its next
|
|
||||||
checkpoint. Code between `.set()` and the next
|
|
||||||
`await` runs atomically from the scheduler's
|
|
||||||
perspective. Use this to your advantage (or watch
|
|
||||||
for bugs where code assumes the woken task runs
|
|
||||||
immediately).
|
|
||||||
|
|
||||||
### `except` block checkpoint masking
|
|
||||||
|
|
||||||
`await` expressions inside `except` handlers can
|
|
||||||
be masked by `trio.Cancelled`. If a `finally`
|
|
||||||
block runs from an `except` and contains
|
|
||||||
`lock.release()`, the release happens — but any
|
|
||||||
`await` after it in the same `except` may be
|
|
||||||
swallowed. This is why `maybe_open_context`'s
|
|
||||||
cache-miss path does `lock.release()` in a
|
|
||||||
`finally` inside the `except KeyError`.
|
|
||||||
|
|
||||||
### Cancellation in `finally`
|
|
||||||
|
|
||||||
Unlike asyncio, trio allows checkpoints in
|
|
||||||
`finally` blocks. This means `finally` cleanup
|
|
||||||
that does `await` can itself be cancelled (e.g.
|
|
||||||
by nursery shutdown). Watch for cleanup code that
|
|
||||||
assumes it will run to completion.
|
|
||||||
|
|
||||||
### Unbounded waits in cleanup paths
|
|
||||||
|
|
||||||
Any `await <event>.wait()` in a teardown path is
|
|
||||||
a latent deadlock unless the event's setter is
|
|
||||||
GUARANTEED to fire. If the setter depends on
|
|
||||||
external state (peer disconnects, child process
|
|
||||||
exit, subsequent task completion) that itself
|
|
||||||
depends on the current task's progress, you have
|
|
||||||
a mutual wait.
|
|
||||||
|
|
||||||
Rule: **bound every `await X.wait()` in cleanup
|
|
||||||
paths with `trio.move_on_after()`** unless you
|
|
||||||
can prove the setter is unconditionally reachable
|
|
||||||
from the state at the await site. Concrete recent
|
|
||||||
example: `ipc_server.wait_for_no_more_peers()` in
|
|
||||||
`async_main`'s finally (see
|
|
||||||
`ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md`
|
|
||||||
"probe iteration 3") — it was unbounded, and when
|
|
||||||
one peer-handler was stuck the wait-for-no-more-
|
|
||||||
peers event never fired, deadlocking the whole
|
|
||||||
actor-tree teardown cascade.
|
|
||||||
|
|
||||||
### The capture-pipe-fill hang pattern (grep this first)
|
|
||||||
|
|
||||||
When investigating any hang in the test suite
|
|
||||||
**especially under fork-based backends**, first
|
|
||||||
check whether the hang reproduces under `pytest
|
|
||||||
-s` (`--capture=no`). If `-s` makes it go away
|
|
||||||
you're not looking at a trio concurrency bug —
|
|
||||||
you're looking at a Linux pipe-buffer fill.
|
|
||||||
|
|
||||||
Mechanism: pytest replaces fds 1,2 with pipe
|
|
||||||
write-ends. Fork-child subactors inherit those
|
|
||||||
fds. High-volume error-log tracebacks (cancel
|
|
||||||
cascade spew) fill the 64KB pipe buffer. Child
|
|
||||||
`write()` blocks. Child can't exit. Parent's
|
|
||||||
`waitpid`/pidfd wait blocks. Deadlock cascades up
|
|
||||||
the tree.
|
|
||||||
|
|
||||||
Pre-existing guards in `tests/conftest.py` encode
|
|
||||||
this knowledge — grep these BEFORE blaming
|
|
||||||
concurrency:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# tests/conftest.py:258
|
|
||||||
if loglevel in ('trace', 'debug'):
|
|
||||||
# XXX: too much logging will lock up the subproc (smh)
|
|
||||||
loglevel: str = 'info'
|
|
||||||
|
|
||||||
# tests/conftest.py:316
|
|
||||||
# can lock up on the `_io.BufferedReader` and hang..
|
|
||||||
stderr: str = proc.stderr.read().decode()
|
|
||||||
```
|
|
||||||
|
|
||||||
Full post-mortem +
|
|
||||||
`ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md`
|
|
||||||
for the canonical reproduction. Cost several
|
|
||||||
investigation sessions before catching it —
|
|
||||||
because the capture-pipe symptom was masked by
|
|
||||||
deeper cascade-deadlocks. Once the cascades were
|
|
||||||
fixed, the tree tore down enough to generate
|
|
||||||
pipe-filling log volume → capture-pipe finally
|
|
||||||
surfaced. Grep-note for future-self: **if a
|
|
||||||
multi-subproc tractor test hangs, `pytest -s`
|
|
||||||
first, conc-anal second.**
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
# PR/Patch-Request Description Format Reference
|
|
||||||
|
|
||||||
Canonical structure for `tractor` patch-request
|
|
||||||
descriptions, designed to work across GitHub,
|
|
||||||
Gitea, SourceHut, and GitLab markdown renderers.
|
|
||||||
|
|
||||||
**Line length: wrap at 72 chars** for all prose
|
|
||||||
content (Summary bullets, Motivation paragraphs,
|
|
||||||
Scopes bullets, etc.). Fill lines *to* 72 — don't
|
|
||||||
stop short at 50-65. Only raw URLs in
|
|
||||||
reference-link definitions may exceed this.
|
|
||||||
|
|
||||||
## Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
<!-- pr-msg-meta
|
|
||||||
branch: <branch-name>
|
|
||||||
base: <base-branch>
|
|
||||||
submitted:
|
|
||||||
github: ___
|
|
||||||
gitea: ___
|
|
||||||
srht: ___
|
|
||||||
-->
|
|
||||||
|
|
||||||
## <Title: present-tense verb + backticked code>
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
- [<hash>][<hash>] Description of change ending
|
|
||||||
with period.
|
|
||||||
- [<hash>][<hash>] Another change description
|
|
||||||
ending with period.
|
|
||||||
- [<hash>][<hash>] [<hash>][<hash>] Multi-commit
|
|
||||||
change description.
|
|
||||||
|
|
||||||
### Motivation
|
|
||||||
<1-2 paragraphs: problem/limitation first,
|
|
||||||
then solution. Hard-wrap at 72 chars.>
|
|
||||||
|
|
||||||
### Scopes changed
|
|
||||||
- [<hash>][<hash>] `pkg.mod.func()` — what
|
|
||||||
changed.
|
|
||||||
* [<hash>][<hash>] Also adjusts
|
|
||||||
`.related_thing()` in same module.
|
|
||||||
- [<hash>][<hash>] `tests.test_mod` — new/changed
|
|
||||||
test coverage.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
### Cross-references
|
|
||||||
Also submitted as
|
|
||||||
[github-pr][] | [gitea-pr][] | [srht-patch][].
|
|
||||||
|
|
||||||
### Links
|
|
||||||
- [relevant-issue-or-discussion](url)
|
|
||||||
- [design-doc-or-screenshot](url)
|
|
||||||
-->
|
|
||||||
|
|
||||||
(this pr content was generated in some part by
|
|
||||||
[`claude-code`][claude-code-gh])
|
|
||||||
|
|
||||||
[<hash>]: https://<service>/<owner>/<repo>/commit/<hash>
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
|
|
||||||
<!-- cross-service pr refs (fill after submit):
|
|
||||||
[github-pr]: https://github.com/<owner>/<repo>/pull/___
|
|
||||||
[gitea-pr]: https://<host>/<owner>/<repo>/pulls/___
|
|
||||||
[srht-patch]: https://git.sr.ht/~<owner>/<repo>/patches/___
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
## Markdown Reference-Link Strategy
|
|
||||||
|
|
||||||
Use reference-style links for ALL commit hashes
|
|
||||||
and cross-service PR refs to ensure cross-service
|
|
||||||
compatibility:
|
|
||||||
|
|
||||||
**Inline usage** (in bullets):
|
|
||||||
```markdown
|
|
||||||
- [f3726cf9][f3726cf9] Add `reg_err_types()`
|
|
||||||
for custom exc lookup.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Definition** (bottom of document):
|
|
||||||
```markdown
|
|
||||||
[f3726cf9]: https://github.com/goodboy/tractor/commit/f3726cf9
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why reference-style?
|
|
||||||
- Keeps prose readable without long inline URLs.
|
|
||||||
- All URLs in one place — trivially swappable
|
|
||||||
per-service.
|
|
||||||
- Most git services auto-link bare SHAs anyway,
|
|
||||||
but explicit refs guarantee it works in *any*
|
|
||||||
md renderer.
|
|
||||||
- The `[hash][hash]` form is self-documenting —
|
|
||||||
display text matches the ref ID.
|
|
||||||
- Cross-service PR refs use the same mechanism:
|
|
||||||
`[github-pr][]` resolves via a ref-link def
|
|
||||||
at the bottom, trivially fillable post-submit.
|
|
||||||
|
|
||||||
## Cross-Service PR Placeholder Mechanism
|
|
||||||
|
|
||||||
The generated description includes three layers
|
|
||||||
of cross-service support, all using native md
|
|
||||||
reference-links:
|
|
||||||
|
|
||||||
### 1. Metadata comment (top of file)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
<!-- pr-msg-meta
|
|
||||||
branch: remote_exc_type_registry
|
|
||||||
base: main
|
|
||||||
submitted:
|
|
||||||
github: ___
|
|
||||||
gitea: ___
|
|
||||||
srht: ___
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
A YAML-ish HTML comment block. The `___`
|
|
||||||
placeholders get filled with PR/patch numbers
|
|
||||||
after submission. Machine-parseable for tooling
|
|
||||||
(e.g. `gish`) but invisible in rendered md.
|
|
||||||
|
|
||||||
### 2. Cross-references section (in body)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
<!--
|
|
||||||
### Cross-references
|
|
||||||
Also submitted as
|
|
||||||
[github-pr][] | [gitea-pr][] | [srht-patch][].
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
Commented out at generation time. After submitting
|
|
||||||
to multiple services, uncomment and the ref-links
|
|
||||||
resolve via the stubs at the bottom.
|
|
||||||
|
|
||||||
### 3. Ref-link stubs (bottom of file)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
<!-- cross-service pr refs (fill after submit):
|
|
||||||
[github-pr]: https://github.com/goodboy/tractor/pull/___
|
|
||||||
[gitea-pr]: https://pikers.dev/goodboy/tractor/pulls/___
|
|
||||||
[srht-patch]: https://git.sr.ht/~goodboy/tractor/patches/___
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
Commented out with `___` number placeholders.
|
|
||||||
After submission: uncomment, replace `___` with
|
|
||||||
the actual number. Each service-specific copy
|
|
||||||
fills in all services' numbers so any copy can
|
|
||||||
cross-reference the others.
|
|
||||||
|
|
||||||
### Post-submission file layout
|
|
||||||
|
|
||||||
```
|
|
||||||
pr_msg_LATEST.md # latest draft (skill root)
|
|
||||||
msgs/
|
|
||||||
20260325T002027Z_mybranch_pr_msg.md # timestamped
|
|
||||||
github/
|
|
||||||
42_pr_msg.md # github PR #42
|
|
||||||
gitea/
|
|
||||||
17_pr_msg.md # gitea PR #17
|
|
||||||
srht/
|
|
||||||
5_pr_msg.md # srht patch #5
|
|
||||||
```
|
|
||||||
|
|
||||||
Each `<service>/<num>_pr_msg.md` is a copy with:
|
|
||||||
- metadata `submitted:` fields filled in
|
|
||||||
- cross-references section uncommented
|
|
||||||
- ref-link stubs uncommented with real numbers
|
|
||||||
- all services cross-linked in each copy
|
|
||||||
|
|
||||||
This mirrors the `gish` skill's
|
|
||||||
`<backend>/<num>.md` pattern.
|
|
||||||
|
|
||||||
## Commit-Link URL Patterns by Service
|
|
||||||
|
|
||||||
| Service | Pattern |
|
|
||||||
|-----------|-------------------------------------|
|
|
||||||
| GitHub | `https://github.com/<o>/<r>/commit/<h>` |
|
|
||||||
| Gitea | `https://<host>/<o>/<r>/commit/<h>` |
|
|
||||||
| SourceHut | `https://git.sr.ht/~<o>/<r>/commit/<h>` |
|
|
||||||
| GitLab | `https://gitlab.com/<o>/<r>/-/commit/<h>` |
|
|
||||||
|
|
||||||
## PR/Patch URL Patterns by Service
|
|
||||||
|
|
||||||
| Service | Pattern |
|
|
||||||
|-----------|-------------------------------------|
|
|
||||||
| GitHub | `https://github.com/<o>/<r>/pull/<n>` |
|
|
||||||
| Gitea | `https://<host>/<o>/<r>/pulls/<n>` |
|
|
||||||
| SourceHut | `https://git.sr.ht/~<o>/<r>/patches/<n>` |
|
|
||||||
| GitLab | `https://gitlab.com/<o>/<r>/-/merge_requests/<n>` |
|
|
||||||
|
|
||||||
## Scope Naming Convention
|
|
||||||
|
|
||||||
Use Python namespace-resolution syntax for
|
|
||||||
referencing changed code scopes:
|
|
||||||
|
|
||||||
| File path | Scope reference |
|
|
||||||
|---------------------------|-------------------------------|
|
|
||||||
| `tractor/_exceptions.py` | `tractor._exceptions` |
|
|
||||||
| `tractor/_state.py` | `tractor._state` |
|
|
||||||
| `tests/test_foo.py` | `tests.test_foo` |
|
|
||||||
| Function in module | `tractor._exceptions.func()` |
|
|
||||||
| Method on class | `.RemoteActorError.src_type` |
|
|
||||||
| Class | `tractor._exceptions.RAE` |
|
|
||||||
|
|
||||||
Prefix with the package path for top-level refs;
|
|
||||||
use leading-dot shorthand (`.ClassName.method()`)
|
|
||||||
for sub-bullets where the parent module is already
|
|
||||||
established.
|
|
||||||
|
|
||||||
## Title Conventions
|
|
||||||
|
|
||||||
Same verb vocabulary as commit messages:
|
|
||||||
- `Add` — wholly new feature/API
|
|
||||||
- `Fix` — bug fix
|
|
||||||
- `Drop` — removal
|
|
||||||
- `Use` — adopt new approach
|
|
||||||
- `Move`/`Mv` — relocate code
|
|
||||||
- `Adjust` — minor tweak
|
|
||||||
- `Update` — enhance existing feature
|
|
||||||
- `Support` — add support for something
|
|
||||||
|
|
||||||
Target 50 chars, hard max 70. Always backtick
|
|
||||||
code elements.
|
|
||||||
|
|
||||||
## Tone
|
|
||||||
|
|
||||||
Casual yet technically precise — matching the
|
|
||||||
project's commit-msg style. Terse but every bullet
|
|
||||||
carries signal. Use project abbreviations freely
|
|
||||||
(msg, bg, ctx, impl, mod, obvi, fn, bc, var,
|
|
||||||
prolly, ep, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
(this format reference was generated by
|
|
||||||
[`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
# Tractor Test Harness Reference
|
|
||||||
|
|
||||||
This repository-local file supplements the canonical [`/run-tests` skill][1]
|
|
||||||
from the [`ai.skillz` repository][2]. Its deployer links the shared `SKILL.md`
|
|
||||||
into both
|
|
||||||
`.claude/skills/run-tests/` and `.opencode/skills/run-tests/` while preserving
|
|
||||||
this project-owned reference:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bash /path/to/ai.skillz/scripts/deploy.sh run-tests /path/to/tractor --provider all --method symlink
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep shared environment permission, process-signal safety, target selection,
|
|
||||||
failure inspection, and result reporting policy in the deployed `SKILL.md`.
|
|
||||||
|
|
||||||
[1]: https://github.com/baudco/ai.skillz/blob/2d4896ca7e38fe2cb3090cdefc7245be4241a6d2/skills/run-tests/SKILL.md
|
|
||||||
[2]: https://github.com/baudco/ai.skillz
|
|
||||||
|
|
||||||
## Project And Environment
|
|
||||||
|
|
||||||
- Project/import: `tractor`
|
|
||||||
- Test root: `tests/`
|
|
||||||
- Supported Python: `>=3.13,<3.15`
|
|
||||||
- Runner: pytest `>=9.0.3`
|
|
||||||
- Test dependencies: the `dev` group includes the `testing` group
|
|
||||||
- CI uses uv's default `.venv`; the Nix flake uses `py313`.
|
|
||||||
- Run from the repository root so pytest loads `pyproject.toml`.
|
|
||||||
- Do not use `default.nix` as current test-environment authority; it still
|
|
||||||
selects unsupported Python 3.12.
|
|
||||||
|
|
||||||
Environment directory naming is not a harness invariant. Use an already
|
|
||||||
verified active project environment when available. Otherwise, use an
|
|
||||||
existing uv environment without syncing it:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync python -c 'import pathlib, sys, tractor; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(tractor.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
|
||||||
```
|
|
||||||
|
|
||||||
After module moves or collection failures, check collection with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest --collect-only -q tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
Collection is not a mandatory precursor to every narrow run. Ask before
|
|
||||||
provisioning or changing an environment.
|
|
||||||
|
|
||||||
Before trusting CLI-selected runtime settings, inspect
|
|
||||||
`TRACTOR_SPAWN_METHOD` and `TRACTOR_LOGLEVEL`. They override the spawn method
|
|
||||||
and runtime log level passed by callers, so report active values with test
|
|
||||||
results rather than claiming the CLI flags alone selected the runtime.
|
|
||||||
|
|
||||||
## Pytest Configuration And Commands
|
|
||||||
|
|
||||||
`pyproject.toml` configures:
|
|
||||||
|
|
||||||
- `testpaths = ["tests"]` and `--rootdir=./tests`;
|
|
||||||
- importlib import mode;
|
|
||||||
- the `tractor._testing.pytest` plugin;
|
|
||||||
- xonsh plugin disablement;
|
|
||||||
- `--show-capture=no` and `--capture=fd`.
|
|
||||||
|
|
||||||
Do not silently add `-x`, `--tb=short`, or `--no-header`; those are not
|
|
||||||
project defaults. In a verified active environment, replace `uv run
|
|
||||||
--frozen --no-sync pytest` below with `python -m pytest`.
|
|
||||||
|
|
||||||
```text
|
|
||||||
# Full suite
|
|
||||||
uv run --frozen --no-sync pytest tests/
|
|
||||||
|
|
||||||
# Narrow file
|
|
||||||
uv run --frozen --no-sync pytest tests/test_local.py
|
|
||||||
|
|
||||||
# Exact node
|
|
||||||
uv run --frozen --no-sync pytest tests/discovery/test_registrar.py::test_reg_then_unreg
|
|
||||||
|
|
||||||
# Keyword selection
|
|
||||||
uv run --frozen --no-sync pytest tests/ -k 'cancel and not slow'
|
|
||||||
|
|
||||||
# Previous failures
|
|
||||||
uv run --frozen --no-sync pytest --lf
|
|
||||||
```
|
|
||||||
|
|
||||||
After verifying that the no-sync environment is current, these pytest
|
|
||||||
arguments match the Linux TCP CI row:
|
|
||||||
|
|
||||||
```text
|
|
||||||
CI=1 uv run --frozen --no-sync pytest tests/ -rsx --spawn-backend=trio --tpt-proto=tcp --capture=fd
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugin Options And Matrices
|
|
||||||
|
|
||||||
Supported spawn backends:
|
|
||||||
|
|
||||||
- `trio` (default)
|
|
||||||
- `mp_spawn`
|
|
||||||
- `mp_forkserver`
|
|
||||||
|
|
||||||
Do not advertise `subint`, `subint_forkserver`, or
|
|
||||||
`main_thread_forkserver` as runnable backends. Supported transports are
|
|
||||||
`tcp` (default) and `uds`. Run one transport per pytest session.
|
|
||||||
`mp_forkserver` and UDS are POSIX-only.
|
|
||||||
|
|
||||||
Other Tractor plugin options include:
|
|
||||||
|
|
||||||
- `--tpdb` / `--debug-mode`
|
|
||||||
- `--ll` / `--loglevel`
|
|
||||||
- `--tl` / `--tractor-loglevel`
|
|
||||||
- `--enable-stackscope`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest tests/ipc/ --tpt-proto=uds
|
|
||||||
uv run --frozen --no-sync pytest tests/test_spawning.py --spawn-backend=mp_spawn
|
|
||||||
uv run --frozen --no-sync pytest tests/test_spawning.py --spawn-backend=mp_forkserver --capture=sys
|
|
||||||
```
|
|
||||||
|
|
||||||
CI currently exercises Python 3.13 with the `trio` backend: TCP and UDS on
|
|
||||||
Linux and macOS, plus an informational TCP row on Windows whose pytest step
|
|
||||||
uses `continue-on-error`.
|
|
||||||
|
|
||||||
## Registry And Transport Isolation
|
|
||||||
|
|
||||||
Tests requesting the `reg_addr` fixture use addresses randomized per session:
|
|
||||||
an unreserved unprivileged loopback port for TCP or a unique socket name under
|
|
||||||
the platform runtime directory for UDS. A TCP collision remains possible.
|
|
||||||
|
|
||||||
The runtime fallback remains `127.0.0.1:1616` or `registry@1616.sock`.
|
|
||||||
Inspect that fallback only when the selected test intentionally uses runtime
|
|
||||||
defaults or a failure identifies that address. Do not perform a mandatory
|
|
||||||
`:1616` preflight or assume UDS sockets live under `/tmp`.
|
|
||||||
|
|
||||||
## Capture And Hang Diagnosis
|
|
||||||
|
|
||||||
Normal capture is `fd`. Use `--capture=sys` with `mp_forkserver`; some tests
|
|
||||||
switch to `capsys`, but the harness does not enforce that suite-wide.
|
|
||||||
|
|
||||||
For a suspected capture interaction, compare only the exact node:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest <node> --capture=sys
|
|
||||||
uv run --frozen --no-sync pytest <node> -s
|
|
||||||
```
|
|
||||||
|
|
||||||
Treat `-s` as a diagnostic comparison, not a pass-equivalent workaround. Do
|
|
||||||
not use it to reinterpret an ordinary captured pass. Interactive `--tpdb` or
|
|
||||||
`tractor.pause()` sessions are different: they require a real TTY and disabled
|
|
||||||
capture, normally `-s`.
|
|
||||||
|
|
||||||
Do not add a global pytest timeout. `fail_after_w_trace` is Trio-cooperative;
|
|
||||||
`afk_alarm_w_trace` is a POSIX main-thread `SIGALRM` hard backstop and can
|
|
||||||
raise asynchronously. Use the latter only as a last resort, not as a generally
|
|
||||||
Trio-safe timeout replacement.
|
|
||||||
|
|
||||||
For live task-tree diagnosis:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync python -c 'import stackscope'
|
|
||||||
uv run --frozen --no-sync pytest <node> --enable-stackscope --capture=sys
|
|
||||||
kill -USR1 <pytest-pid>
|
|
||||||
```
|
|
||||||
|
|
||||||
The import check and pytest command must use the same environment. Do not send
|
|
||||||
SIGUSR1 if the import fails or setup warns that stackscope or SIGUSR1 is
|
|
||||||
unavailable: without the installed handler, SIGUSR1 normally terminates the
|
|
||||||
target process. Signal a subactor only after separately confirming that it
|
|
||||||
installed the same handler.
|
|
||||||
|
|
||||||
Stackscope appends dumps to `/tmp/tractor-stackscope-<pid>.log`, including when
|
|
||||||
pytest capture hides terminal output. SIGUSR1 stackscope is unavailable on
|
|
||||||
Windows and degrades to a no-op there.
|
|
||||||
|
|
||||||
When a trace guard actually fires and snapshot capture succeeds, it writes
|
|
||||||
under `$XDG_CACHE_HOME/tractor/hung-dumps/`, falling back beneath
|
|
||||||
`~/.cache/tractor/hung-dumps/`, and prints an end-of-session index. A normal
|
|
||||||
non-timeout run creates no snapshot.
|
|
||||||
|
|
||||||
## Cleanup And `tractor-reap`
|
|
||||||
|
|
||||||
On Linux, normal pytest teardown discovers surviving descendants through
|
|
||||||
`/proc`, sends SIGINT, waits three seconds, then escalates survivors to
|
|
||||||
SIGKILL. It does not sweep shared memory and cannot run if pytest never reaches
|
|
||||||
fixture teardown.
|
|
||||||
|
|
||||||
Process discovery is a no-op off Linux. UDS PID liveness also depends on
|
|
||||||
`/proc`; on macOS, recognized PID-named sockets can therefore be classified as
|
|
||||||
dead without proof. The session-scoped autouse fixture currently passes those
|
|
||||||
candidates directly to `reap_uds()` at teardown. Do not treat its non-Linux
|
|
||||||
classification as proof of orphanhood or run concurrent live Tractor sessions
|
|
||||||
against the same UDS bindspace.
|
|
||||||
|
|
||||||
Use the CLI in inspection-only mode first:
|
|
||||||
|
|
||||||
`--shm` and `--shm-only` are Linux/FreeBSD-only and raise
|
|
||||||
`NotImplementedError` elsewhere. On other platforms, use the UDS-only command.
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --parent <pytest-pid> -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --shm --uds -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --uds-only -n
|
|
||||||
```
|
|
||||||
|
|
||||||
Direct `scripts/tractor-reap` execution is acceptable only after verifying its
|
|
||||||
`python3` shebang resolves the intended project environment.
|
|
||||||
|
|
||||||
Review every candidate before requesting a mutating run:
|
|
||||||
|
|
||||||
- default orphan mode is not repository-scoped;
|
|
||||||
- `--parent` trusts the supplied PID and can include non-Tractor children;
|
|
||||||
- `--shm` scans all current-user candidate files, not just Tractor-named
|
|
||||||
files;
|
|
||||||
- `--uds` treats `registry@1616.sock` as removable even if a live default UDS
|
|
||||||
registrar uses it.
|
|
||||||
|
|
||||||
Dry-run output prints only the initially matched root PIDs. A mutating run can
|
|
||||||
recursively expand those roots to additional descendants when `psutil` is
|
|
||||||
available. Inspect the descendant process tree separately; `-n` is not exact
|
|
||||||
signal-set parity and does not by itself authorize signaling unseen children.
|
|
||||||
|
|
||||||
The canonical skill owns signaling and unlinking authorization.
|
|
||||||
|
|
||||||
## Test Layout And Change Mapping
|
|
||||||
|
|
||||||
| Changed area | Run first |
|
|
||||||
|---|---|
|
|
||||||
| `tractor/runtime/_runtime.py`, `_state.py`, `tractor/_root.py` | `tests/test_local.py`, `tests/test_root_runtime.py`, `tests/test_runtime.py`, `tests/test_rpc.py` |
|
|
||||||
| `tractor/runtime/_portal.py`, `_rpc.py` | `tests/test_rpc.py`, `tests/test_cancellation.py` |
|
|
||||||
| `tractor/runtime/_supervise.py` | `tests/test_cancellation.py`, `tests/test_spawning.py` |
|
|
||||||
| `tractor/discovery/` | `tests/discovery/`, `tests/test_local.py` |
|
|
||||||
| `tractor/ipc/` | `tests/ipc/`, `tests/test_2way.py`, `tests/test_shm.py` as relevant |
|
|
||||||
| `tractor/spawn/` | `tests/test_spawning.py`, `tests/discovery/test_multi_program.py`, `tests/test_cancellation.py` |
|
|
||||||
| `tractor/_context.py`, `_streaming.py` | `tests/test_context_stream_semantics.py`, `tests/test_advanced_streaming.py`, `tests/test_legacy_one_way_streaming.py` |
|
|
||||||
| `tractor/to_asyncio.py` | `tests/test_infected_asyncio.py`, `tests/test_root_infect_asyncio.py` |
|
|
||||||
| `tractor/msg/` | `tests/msg/` |
|
|
||||||
| `tractor/devx/` | `tests/devx/`; debugger tests use pexpect and are comparatively slow |
|
|
||||||
| `tractor/_exceptions.py` | `tests/test_remote_exc_relay.py`, `tests/test_reg_err_types.py`, `tests/test_inter_peer_cancellation.py`, `tests/test_cancellation.py`, `tests/msg/` |
|
|
||||||
|
|
||||||
Current subdirectories include `discovery/`, `ipc/`, `msg/`, `devx/`, and
|
|
||||||
`trionics/`. There is no `tests/spawn/` directory.
|
|
||||||
|
|
||||||
## Expected Outcomes
|
|
||||||
|
|
||||||
Do not maintain a blanket known-flaky exemption list. Classify only current
|
|
||||||
explicit skip or xfail marks and exact expected signatures. Notable tracked
|
|
||||||
outcomes include:
|
|
||||||
|
|
||||||
- duplicate-name `n_dups=4` and `n_dups=8` variants in
|
|
||||||
`tests/discovery/test_multi_program.py` are non-strict xfails;
|
|
||||||
- `tests/test_ringbuf.py` is module-skipped;
|
|
||||||
- some documentation examples have explicit macOS-CI skips.
|
|
||||||
|
|
||||||
A generic `TooSlowError` or `pexpect.TIMEOUT` is not enough to classify a
|
|
||||||
failure as pre-existing.
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
# NOTE distilled from,
|
|
||||||
# https://github.com/orgs/community/discussions/26276
|
|
||||||
on:
|
on:
|
||||||
# any time a new update to 'main'
|
# any time someone pushes a new branch to origin
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
# for on all (forked) PRs to repo
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
# NOTE, use a draft PR if you just want CI triggered..
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
# to run workflow manually from the "Actions" tab
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -37,12 +29,7 @@ jobs:
|
||||||
run: uv build --sdist --python=3.13
|
run: uv build --sdist --python=3.13
|
||||||
|
|
||||||
- name: Install sdist from .tar.gz
|
- name: Install sdist from .tar.gz
|
||||||
# XXX must install under py3.13 (matching the build's
|
run: python -m pip install dist/*.tar.gz
|
||||||
# `--python=3.13`); the runner's default `python` is 3.12
|
|
||||||
# which our `requires-python = ">=3.13"` now rejects.
|
|
||||||
run: |
|
|
||||||
uv venv --python 3.13
|
|
||||||
uv pip install dist/*.tar.gz
|
|
||||||
|
|
||||||
# ------ type-check ------
|
# ------ type-check ------
|
||||||
# mypy:
|
# mypy:
|
||||||
|
|
@ -87,49 +74,24 @@ jobs:
|
||||||
# run: mypy tractor/ --ignore-missing-imports --show-traceback
|
# run: mypy tractor/ --ignore-missing-imports --show-traceback
|
||||||
|
|
||||||
|
|
||||||
testing:
|
testing-linux:
|
||||||
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
name: '${{ matrix.os }} Python ${{ matrix.python }} - ${{ matrix.spawn_backend }}'
|
||||||
timeout-minutes: 16
|
timeout-minutes: 10
|
||||||
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
|
||||||
matrix:
|
matrix:
|
||||||
os: [
|
os: [ubuntu-latest]
|
||||||
ubuntu-latest,
|
python-version: ['3.13']
|
||||||
macos-latest,
|
|
||||||
windows-latest,
|
|
||||||
]
|
|
||||||
python-version: [
|
|
||||||
'3.13',
|
|
||||||
# '3.14',
|
|
||||||
]
|
|
||||||
spawn_backend: [
|
spawn_backend: [
|
||||||
'trio',
|
'trio',
|
||||||
# 'mp_spawn',
|
# 'mp_spawn',
|
||||||
# 'mp_forkserver',
|
# 'mp_forkserver',
|
||||||
# ?TODO^ is it worth it to get these running again?
|
|
||||||
#
|
|
||||||
# - [ ] next-gen backends, on 3.13+
|
|
||||||
# https://github.com/goodboy/tractor/issues/379
|
|
||||||
# 'subinterpreter',
|
|
||||||
# 'subint',
|
|
||||||
]
|
]
|
||||||
tpt_proto: [
|
|
||||||
'tcp',
|
|
||||||
'uds',
|
|
||||||
]
|
|
||||||
exclude:
|
|
||||||
# UDS is POSIX-only; Windows has no `AF_UNIX` so the
|
|
||||||
# backend is intentionally unavailable there.
|
|
||||||
- os: windows-latest
|
|
||||||
tpt_proto: 'uds'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 'Install uv + py-${{ matrix.python-version }}'
|
- name: 'Install uv + py-${{ matrix.python-version }}'
|
||||||
|
|
@ -155,28 +117,8 @@ 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
|
run: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx
|
||||||
# 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: >
|
|
||||||
uv run
|
|
||||||
pytest
|
|
||||||
tests/
|
|
||||||
-rsx
|
|
||||||
--spawn-backend=${{ matrix.spawn_backend }}
|
|
||||||
--tpt-proto=${{ matrix.tpt_proto }}
|
|
||||||
--capture=fd
|
|
||||||
--reruns=${{ matrix.os == 'macos-latest' && 2 || 0 }}
|
|
||||||
--reruns-delay=1
|
|
||||||
|
|
||||||
# XXX legacy NOTE XXX
|
# XXX legacy NOTE XXX
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
name: docs
|
|
||||||
|
|
||||||
# build sphinx docs on every PR + push to main;
|
|
||||||
# deploy to gh-pages only from main pushes.
|
|
||||||
# (see goodboy/tractor#123 for the original ask)
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
# to run workflow manually from the "Actions" tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# needed by actions/deploy-pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: 'sphinx build'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install latest uv
|
|
||||||
uses: astral-sh/setup-uv@v6
|
|
||||||
|
|
||||||
# NOTE, no `d2` bin is installed in CI (yet) so
|
|
||||||
# the pre-rendered + committed SVGs under
|
|
||||||
# `docs/_diagrams/` are used as-is; see
|
|
||||||
# `docs/_ext/d2diagrams.py` for the fallback
|
|
||||||
# policy.
|
|
||||||
- name: Build html docs
|
|
||||||
run: |
|
|
||||||
uv sync --no-dev --group docs
|
|
||||||
uv run --no-dev --group docs make -C docs html
|
|
||||||
|
|
||||||
- name: Upload pages artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: docs/_build/html
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: 'deploy to gh-pages'
|
|
||||||
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
|
||||||
needs: build
|
|
||||||
# serialize deploys but NEVER cancel an in-flight one
|
|
||||||
# mid-upload; queue the next instead (scoped to this job so
|
|
||||||
# PR builds can't cancel a production deploy).
|
|
||||||
concurrency:
|
|
||||||
group: 'pages'
|
|
||||||
cancel-in-progress: false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- name: Deploy
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
|
|
@ -102,333 +102,3 @@ venv.bak/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
# all files under
|
|
||||||
.git/
|
|
||||||
|
|
||||||
# require very explicit staging for anything we **really**
|
|
||||||
# want put/kept in repo.
|
|
||||||
notes_to_self/
|
|
||||||
snippets/
|
|
||||||
|
|
||||||
# ------- AI shiz -------
|
|
||||||
# `ai.skillz` symlinks,
|
|
||||||
# (machine-local, deploy via deploy-skill.sh)
|
|
||||||
.claude/skills/py-codestyle
|
|
||||||
.claude/skills/close-wkt
|
|
||||||
.claude/skills/plan-io
|
|
||||||
.claude/skills/prompt-io
|
|
||||||
.claude/skills/resolve-conflicts
|
|
||||||
.claude/skills/inter-skill-review
|
|
||||||
|
|
||||||
# /open-wkt specifics
|
|
||||||
.claude/skills/open-wkt
|
|
||||||
.claude/wkts/
|
|
||||||
claude_wkts
|
|
||||||
|
|
||||||
# /code-review-changes specifics
|
|
||||||
.claude/skills/code-review-changes
|
|
||||||
# review-skill ephemeral ctx (per-PR, single-use)
|
|
||||||
.claude/review_context.md
|
|
||||||
.claude/review_regression.md
|
|
||||||
|
|
||||||
# /pr-msg specifics
|
|
||||||
.claude/skills/pr-msg/*
|
|
||||||
# repo-specific
|
|
||||||
!.claude/skills/pr-msg/format-reference.md
|
|
||||||
# XXX, so u can nvim-telescope this file.
|
|
||||||
# !.claude/skills/pr-msg/pr_msg_LATEST.md
|
|
||||||
|
|
||||||
# /commit-msg specifics
|
|
||||||
# - any commit-msg gen tmp files
|
|
||||||
.claude/*_commit_*.md
|
|
||||||
.claude/*_commit*.txt
|
|
||||||
.claude/skills/commit-msg/*
|
|
||||||
!.claude/skills/commit-msg/style-duie-reference.md
|
|
||||||
|
|
||||||
# use prompt-io instead?
|
|
||||||
.claude/plans
|
|
||||||
|
|
||||||
# nix develop --profile .nixdev
|
|
||||||
.nixdev*
|
|
||||||
|
|
||||||
# :Obsession .
|
|
||||||
Session.vim
|
|
||||||
|
|
||||||
# `gish` local `.md`-files
|
|
||||||
# TODO? better all around automation!
|
|
||||||
# -[ ] it'd be handy to also commit and sync with wtv git service?
|
|
||||||
# -[ ] everything should be put under a `.gish/` no?
|
|
||||||
gitea/
|
|
||||||
gh/
|
|
||||||
|
|
||||||
# ------ macOS ------
|
|
||||||
# Finder metadata
|
|
||||||
**/.DS_Store
|
|
||||||
|
|
||||||
# LLM conversations that should remain private
|
|
||||||
docs/conversations/
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:run-tests
|
|
||||||
/.claude/skills/run-tests/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:claude:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:run-tests
|
|
||||||
/.opencode/skills/run-tests/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:run-tests
|
|
||||||
/.opencode/commands/run-tests.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:gish
|
|
||||||
/.claude/skills/gish
|
|
||||||
# END ai.skillz: direct:symlink:claude:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:gish
|
|
||||||
/.opencode/skills/gish
|
|
||||||
# END ai.skillz: direct:symlink:opencode:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:resolve-conflicts
|
|
||||||
/.claude/skills/resolve-conflicts
|
|
||||||
# END ai.skillz: direct:symlink:claude:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:resolve-conflicts
|
|
||||||
/.opencode/skills/resolve-conflicts
|
|
||||||
# END ai.skillz: direct:symlink:opencode:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:git-mgmt
|
|
||||||
/.claude/skills/git-mgmt
|
|
||||||
# END ai.skillz: direct:symlink:claude:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:git-mgmt
|
|
||||||
/.opencode/skills/git-mgmt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:open-wkt
|
|
||||||
/wkts/
|
|
||||||
# END ai.skillz: runtime:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:open-wkt
|
|
||||||
/.claude/skills/open-wkt
|
|
||||||
# END ai.skillz: direct:symlink:claude:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:open-wkt
|
|
||||||
/.opencode/skills/open-wkt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:close-wkt
|
|
||||||
/.claude/skills/close-wkt
|
|
||||||
# END ai.skillz: direct:symlink:claude:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:close-wkt
|
|
||||||
/.opencode/skills/close-wkt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:code-review
|
|
||||||
.ai/code-review/reports/
|
|
||||||
# END ai.skillz: runtime:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-review
|
|
||||||
/.claude/skills/code-review
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review
|
|
||||||
/.opencode/skills/code-review
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-nav-refs
|
|
||||||
/.claude/skills/code-nav-refs
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-nav-refs
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-nav-refs
|
|
||||||
/.opencode/skills/code-nav-refs
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-nav-refs
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:code-review-changes
|
|
||||||
.claude/review_context.md
|
|
||||||
.claude/review_regression.md
|
|
||||||
.claude/review_replies/
|
|
||||||
# END ai.skillz: runtime:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-review-changes
|
|
||||||
/.claude/skills/code-review-changes
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review-changes
|
|
||||||
/.opencode/skills/code-review-changes
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:commit-msg
|
|
||||||
.claude/skills/commit-msg/msgs/
|
|
||||||
.claude/git_commit_msg_LATEST.md
|
|
||||||
# END ai.skillz: runtime:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:commit-msg
|
|
||||||
/.claude/skills/commit-msg/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:claude:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-msg
|
|
||||||
/.opencode/skills/commit-msg/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:commit-plan
|
|
||||||
/.claude/skills/commit-plan
|
|
||||||
# END ai.skillz: direct:symlink:claude:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-plan
|
|
||||||
/.opencode/skills/commit-plan
|
|
||||||
# END ai.skillz: direct:symlink:opencode:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:dep-supersede-scan
|
|
||||||
/.claude/skills/dep-supersede-scan
|
|
||||||
# END ai.skillz: direct:symlink:claude:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
|
||||||
/.opencode/skills/dep-supersede-scan
|
|
||||||
# END ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:harness-perf
|
|
||||||
/.claude/skills/harness-perf
|
|
||||||
# END ai.skillz: direct:symlink:claude:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:harness-perf
|
|
||||||
/.opencode/skills/harness-perf
|
|
||||||
# END ai.skillz: direct:symlink:opencode:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:inter-skill-review
|
|
||||||
/.claude/skills/inter-skill-review
|
|
||||||
# END ai.skillz: direct:symlink:claude:inter-skill-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:inter-skill-review
|
|
||||||
/.opencode/skills/inter-skill-review
|
|
||||||
# END ai.skillz: direct:symlink:opencode:inter-skill-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:opencode-cleaning
|
|
||||||
/.claude/skills/opencode-cleaning
|
|
||||||
# END ai.skillz: direct:symlink:claude:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:opencode-cleaning
|
|
||||||
/.opencode/skills/opencode-cleaning
|
|
||||||
# END ai.skillz: direct:symlink:opencode:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:plan-io
|
|
||||||
/.claude/skills/plan-io
|
|
||||||
# END ai.skillz: direct:symlink:claude:plan-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:plan-io
|
|
||||||
/.opencode/skills/plan-io
|
|
||||||
# END ai.skillz: direct:symlink:opencode:plan-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:pr-msg
|
|
||||||
.claude/skills/pr-msg/msgs/
|
|
||||||
.claude/skills/pr-msg/pr_msg_LATEST.md
|
|
||||||
# END ai.skillz: runtime:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:pr-msg
|
|
||||||
/.claude/skills/pr-msg/SKILL.md
|
|
||||||
/.claude/skills/pr-msg/references
|
|
||||||
/.claude/skills/pr-msg/scripts
|
|
||||||
# END ai.skillz: direct:symlink:claude:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:pr-msg
|
|
||||||
/.opencode/skills/pr-msg/SKILL.md
|
|
||||||
/.opencode/skills/pr-msg/references
|
|
||||||
/.opencode/skills/pr-msg/scripts
|
|
||||||
# END ai.skillz: direct:symlink:opencode:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:prompt-io
|
|
||||||
/.claude/skills/prompt-io
|
|
||||||
# END ai.skillz: direct:symlink:claude:prompt-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:prompt-io
|
|
||||||
/.opencode/skills/prompt-io
|
|
||||||
# END ai.skillz: direct:symlink:opencode:prompt-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:py-codestyle
|
|
||||||
/.claude/skills/py-codestyle
|
|
||||||
# END ai.skillz: direct:symlink:claude:py-codestyle
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:py-codestyle
|
|
||||||
/.opencode/skills/py-codestyle
|
|
||||||
# END ai.skillz: direct:symlink:opencode:py-codestyle
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:taken-export
|
|
||||||
.ai/taken/exports/
|
|
||||||
# END ai.skillz: runtime:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:taken-export
|
|
||||||
/.claude/skills/taken-export
|
|
||||||
# END ai.skillz: direct:symlink:claude:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:taken-export
|
|
||||||
/.opencode/skills/taken-export
|
|
||||||
# END ai.skillz: direct:symlink:opencode:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:yt-url-lookup
|
|
||||||
/.claude/skills/yt-url-lookup
|
|
||||||
# END ai.skillz: direct:symlink:claude:yt-url-lookup
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:yt-url-lookup
|
|
||||||
/.opencode/skills/yt-url-lookup
|
|
||||||
# END ai.skillz: direct:symlink:opencode:yt-url-lookup
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:gish
|
|
||||||
/.opencode/commands/gish.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
|
||||||
/.opencode/commands/resolve-conflicts.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:git-mgmt
|
|
||||||
/.opencode/commands/git-mgmt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:open-wkt
|
|
||||||
/.opencode/commands/open-wkt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:close-wkt
|
|
||||||
/.opencode/commands/close-wkt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review
|
|
||||||
/.opencode/commands/code-review.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review-changes
|
|
||||||
/.opencode/commands/code-review-changes.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-msg
|
|
||||||
/.opencode/commands/commit-msg.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-plan
|
|
||||||
/.opencode/commands/commit-plan.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
|
||||||
/.opencode/commands/dep-supersede-scan.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:harness-perf
|
|
||||||
/.opencode/commands/harness-perf.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
|
||||||
/.opencode/commands/opencode-cleaning.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:pr-msg
|
|
||||||
/.opencode/commands/pr-msg.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:taken-export
|
|
||||||
/.opencode/commands/taken-export.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
|
||||||
/.opencode/commands/yt-url-lookup.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
|
||||||
|
|
|
||||||
1
NEWS.rst
1
NEWS.rst
|
|
@ -78,7 +78,6 @@ Bug Fixes
|
||||||
for ``asyncio``-side errors to not propagate due to a race condition.
|
for ``asyncio``-side errors to not propagate due to a race condition.
|
||||||
|
|
||||||
The implementation fix summary is:
|
The implementation fix summary is:
|
||||||
|
|
||||||
- add state to signal the end of the ``trio`` side task to be
|
- add state to signal the end of the ``trio`` side task to be
|
||||||
read by the ``asyncio`` side and always cancel any ongoing
|
read by the ``asyncio`` side and always cancel any ongoing
|
||||||
task in such cases.
|
task in such cases.
|
||||||
|
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
# `fork()` in a multi-threaded program — execution-side vs. memory-side of the same coin
|
|
||||||
|
|
||||||
A reference doc for readers who've encountered one of two
|
|
||||||
opposite-sounding framings of POSIX `fork()` semantics in a
|
|
||||||
multi-threaded program and are confused by the other.
|
|
||||||
|
|
||||||
This is a sibling to
|
|
||||||
`subint_fork_blocked_by_cpython_post_fork_issue.md` — that
|
|
||||||
doc covers a CPython-level refusal of fork-from-subint;
|
|
||||||
this one covers the more general POSIX layer, since
|
|
||||||
tractor's main-thread forkserver design rests on it.
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
POSIX `fork()` only preserves the *calling* thread as a
|
|
||||||
runnable thread in the child — every other thread in the
|
|
||||||
parent simply never executes another instruction in the
|
|
||||||
child. trio's docs call this "leaked"; tractor's
|
|
||||||
`_main_thread_forkserver.py` docstring calls it "gone".
|
|
||||||
Both are correct: "gone" is the *execution* side (no
|
|
||||||
scheduler entry, no instructions retired), "leaked" is the
|
|
||||||
*memory* side (the dead threads' stacks and per-thread
|
|
||||||
heap structures still ride into the child's address space
|
|
||||||
as orphaned COW pages with no owner and no cleanup hook).
|
|
||||||
Same POSIX reality, two halves of the same coin.
|
|
||||||
|
|
||||||
## The two framings
|
|
||||||
|
|
||||||
[python-trio/trio#1614][trio-1614] (the canonical "trio +
|
|
||||||
fork" hazards thread) puts it this way:
|
|
||||||
|
|
||||||
> If you use `fork()` in a process with multiple threads,
|
|
||||||
> all the other thread stacks are just leaked: there's
|
|
||||||
> nothing else you can reasonably do with them.
|
|
||||||
|
|
||||||
`tractor.spawn._main_thread_forkserver`'s module docstring
|
|
||||||
(specifically the "What survives the fork? — POSIX
|
|
||||||
semantics" section) puts it this way:
|
|
||||||
|
|
||||||
> POSIX `fork()` only preserves the *calling* thread as a
|
|
||||||
> runnable thread in the child. Every other thread in the
|
|
||||||
> parent — trio's runner thread, any `to_thread` cache
|
|
||||||
> threads, anything else — never executes another
|
|
||||||
> instruction post-fork.
|
|
||||||
|
|
||||||
A reader bouncing between the two can be forgiven for
|
|
||||||
asking: well, *which* is it — leaked or gone?
|
|
||||||
|
|
||||||
The answer is "yes". They're describing the same POSIX
|
|
||||||
behavior from two different angles:
|
|
||||||
|
|
||||||
- trio is talking about the **bytes** the dead threads
|
|
||||||
leave behind — stacks, TLS slots, per-thread arena
|
|
||||||
metadata — and the fact that nothing in the child can
|
|
||||||
drive them forward, free them, or even safely walk
|
|
||||||
them. That's a memory leak in the strict sense: held
|
|
||||||
but unreachable.
|
|
||||||
- tractor is talking about the **execution** side
|
|
||||||
relevant to the forkserver design: which threads
|
|
||||||
retire instructions in the child? Exactly one — the
|
|
||||||
one that called `fork()`. Everything else, regardless
|
|
||||||
of the bytes left behind, is dead in a scheduler
|
|
||||||
sense.
|
|
||||||
|
|
||||||
Neither framing is wrong; they're just answering
|
|
||||||
different questions.
|
|
||||||
|
|
||||||
## POSIX `fork()` in a multi-threaded program — what actually happens
|
|
||||||
|
|
||||||
Per POSIX (and concretely on Linux glibc), the contract
|
|
||||||
of `fork()` in a multi-threaded process is:
|
|
||||||
|
|
||||||
1. The kernel creates a new process whose virtual
|
|
||||||
address space is a COW copy of the parent's. *All*
|
|
||||||
pages map across — code, heap, every thread's stack,
|
|
||||||
every malloc arena, every mmap region.
|
|
||||||
2. Of the parent's N threads, exactly **one** is
|
|
||||||
reified in the child as a runnable kernel task: the
|
|
||||||
thread that called `fork()`. The other N-1 threads
|
|
||||||
have *no* corresponding task in the child kernel. They
|
|
||||||
were never scheduled, never `clone()`d for the child,
|
|
||||||
never exist as runnable entities.
|
|
||||||
3. Their **memory artifacts** — pthread stacks, TLS,
|
|
||||||
`pthread_t` structures, glibc per-thread arena
|
|
||||||
bookkeeping — are still mapped in the child's address
|
|
||||||
space, because (1) duplicates *everything* page-wise.
|
|
||||||
They sit there as inert COW bytes.
|
|
||||||
4. The kernel does not clean those bytes up. There is no
|
|
||||||
"phantom-thread cleanup" pass post-fork. The kernel
|
|
||||||
doesn't know which mapped pages "belonged to" which
|
|
||||||
thread — at the kernel level mappings are
|
|
||||||
process-scoped, not thread-scoped.
|
|
||||||
5. The surviving thread (the caller of `fork()`) cannot
|
|
||||||
safely access those leaked bytes either. Any state
|
|
||||||
they encoded — held mutexes, in-flight syscalls,
|
|
||||||
half-updated invariants — is frozen at whatever
|
|
||||||
instant the parent's fork-syscall observed it. Some
|
|
||||||
of those mutexes may even still be locked from the
|
|
||||||
child's POV (the canonical "fork-in-multithreaded-
|
|
||||||
program-deadlocks" hazard; see `man pthread_atfork`).
|
|
||||||
|
|
||||||
So: from the kernel's PoV, the child has one thread.
|
|
||||||
From the address-space's PoV, the child has all the
|
|
||||||
parent's bytes — including the corpses of the N-1 dead
|
|
||||||
threads' stacks. Both true simultaneously.
|
|
||||||
|
|
||||||
## Why trio says "leaked"
|
|
||||||
|
|
||||||
trio's framing makes sense from the parent's
|
|
||||||
PoV, looking at *what those threads were doing*. In a
|
|
||||||
running `trio.run()` process you typically have:
|
|
||||||
|
|
||||||
- The trio runner thread itself — owns the `selectors`
|
|
||||||
epoll fd, the signal-wakeup-fd, the run-queue.
|
|
||||||
- Threadpool worker threads (`trio.to_thread`'s cache)
|
|
||||||
— blocked in `wait()` on the threadpool's work
|
|
||||||
condvar.
|
|
||||||
- Whatever other ad-hoc threads the application
|
|
||||||
started.
|
|
||||||
|
|
||||||
Each of those threads owns *real work-state*: epoll
|
|
||||||
registrations, file descriptors held in
|
|
||||||
soon-to-be-completed reads, half-released locks, posted
|
|
||||||
but unconsumed wakeups. After fork, that state is still
|
|
||||||
encoded in the child's memory. None of it is invalid in
|
|
||||||
a well-formed-bytes sense. It's just that:
|
|
||||||
|
|
||||||
- The thread that was driving it is gone.
|
|
||||||
- Nothing else in the child knows the layout well
|
|
||||||
enough to take over.
|
|
||||||
- Even if it did, the kernel objects backing the work
|
|
||||||
(epoll fd, signalfd) have separate post-fork
|
|
||||||
semantics that don't compose with userland trio
|
|
||||||
state.
|
|
||||||
|
|
||||||
So the bytes are *held* (they're in the child's
|
|
||||||
address space, they count against RSS, they survive
|
|
||||||
until something clobbers them), and they're
|
|
||||||
*unreachable* in any meaningful sense — no thread can
|
|
||||||
safely drive them forward. That is the textbook
|
|
||||||
definition of a leak.
|
|
||||||
|
|
||||||
trio's quote is reminding the user that `fork()` from a
|
|
||||||
multi-threaded process is a one-way memory hazard:
|
|
||||||
whatever those threads were doing, that work-state is
|
|
||||||
now garbage you happen to still be carrying.
|
|
||||||
|
|
||||||
## Why tractor says "gone"
|
|
||||||
|
|
||||||
tractor's `_main_thread_forkserver` framing is concerned
|
|
||||||
with a different question: *which thread executes in the
|
|
||||||
child, and is it safe?*
|
|
||||||
|
|
||||||
The forkserver design rests on POSIX's "calling thread
|
|
||||||
is the sole survivor" guarantee. We pick that calling
|
|
||||||
thread very deliberately: a dedicated worker that has
|
|
||||||
provably never entered trio. So the thread that *does*
|
|
||||||
run in the child is one whose locals, TLS, and stack
|
|
||||||
contain nothing trio-related. Trio's runner thread —
|
|
||||||
the one that owned the epoll fd and the run-queue — is
|
|
||||||
*gone* from the child in the execution sense. It will
|
|
||||||
never run another instruction. The fact that its stack
|
|
||||||
bytes still exist in the child's address space (the
|
|
||||||
"leaked" view) is irrelevant to the forkserver, because
|
|
||||||
nothing in the child reads or writes those pages.
|
|
||||||
|
|
||||||
So when the docstring says "Every other thread … is
|
|
||||||
gone the instant `fork()` returns in the child", it's
|
|
||||||
being precise about the surface that matters for the
|
|
||||||
backend: scheduler-level liveness. Nothing schedules
|
|
||||||
those threads ever again. Whether their bytes are
|
|
||||||
hanging around is a separate (and, for the design,
|
|
||||||
non-load-bearing) fact.
|
|
||||||
|
|
||||||
## Cross-table
|
|
||||||
|
|
||||||
The same tabular layout the `_main_thread_forkserver`
|
|
||||||
docstring uses, expanded with a fourth "what handles
|
|
||||||
it" column:
|
|
||||||
|
|
||||||
| thread | parent | child (executing) | child (memory) | what handles it |
|
|
||||||
|---------------------|-----------|-------------------|------------------------------|-----------------------------|
|
|
||||||
| forkserver worker | continues | sole survivor | live stack | runs the child's bootstrap |
|
|
||||||
| `trio.run()` thread | continues | not running | leaked stack (zombie bytes) | overwritten by child's fresh `trio.run()` |
|
|
||||||
| any other thread | continues | not running | leaked stack (zombie bytes) | overwritten / GC'd / clobbered by `exec()` if used |
|
|
||||||
|
|
||||||
The "child (executing)" column is the *execution* side
|
|
||||||
of the coin — what tractor cares about. The "child
|
|
||||||
(memory)" column is the *memory* side — what trio
|
|
||||||
cares about.
|
|
||||||
|
|
||||||
The "what handles it" column is the deliberate punchline
|
|
||||||
of the design: nothing has to handle the leaked bytes
|
|
||||||
*explicitly*. They get clobbered by ordinary forward
|
|
||||||
progress in the child:
|
|
||||||
|
|
||||||
- The fresh `trio.run()` the child boots up allocates
|
|
||||||
its own stack, scheduler, and run-queue, which over
|
|
||||||
time overlaps and overwrites the inherited zombie
|
|
||||||
pages.
|
|
||||||
- Python's GC walks live objects only; the dead-thread
|
|
||||||
Python frames aren't reachable from any
|
|
||||||
`PyThreadState`, so they get freed at the next
|
|
||||||
collection cycle.
|
|
||||||
- If the child eventually `exec()`s, the entire address
|
|
||||||
space is replaced and the leak vanishes.
|
|
||||||
|
|
||||||
## What this means for the forkserver design
|
|
||||||
|
|
||||||
The crucial point is that **the design doesn't and
|
|
||||||
*can't* prevent the leak**. There is no userland fix
|
|
||||||
for COW thread stacks. The kernel hands the child a
|
|
||||||
duplicated address space; that's what `fork()` *is*. No
|
|
||||||
amount of pre-fork hookery, `pthread_atfork()`
|
|
||||||
gymnastics, or post-fork cleanup can un-COW the dead
|
|
||||||
threads' pages without unmapping them, and unmapping
|
|
||||||
arbitrary regions of a duplicated address space is
|
|
||||||
neither portable nor safe.
|
|
||||||
|
|
||||||
What the design *does* ensure is the orthogonal
|
|
||||||
property: the survivor thread is one that doesn't need
|
|
||||||
any of that leaked state to function. Concretely:
|
|
||||||
|
|
||||||
- Survivor is the forkserver worker thread.
|
|
||||||
- That worker has provably never imported, called into,
|
|
||||||
or held any reference to `trio`. (Enforced by keeping
|
|
||||||
the worker's lifecycle entirely in
|
|
||||||
`_main_thread_forkserver.py` and never letting trio
|
|
||||||
task-state cross into it.)
|
|
||||||
- So the leaked pages — trio runner stack, threadpool
|
|
||||||
caches, etc. — are inert relative to the survivor.
|
|
||||||
No code path in the child references them.
|
|
||||||
- The child then boots its own fresh `trio.run()`,
|
|
||||||
which allocates new state in new pages. Over the
|
|
||||||
child's lifetime the COW'd zombie pages get
|
|
||||||
overwritten, GC'd, or (if the child eventually
|
|
||||||
`exec()`s) discarded wholesale.
|
|
||||||
|
|
||||||
The "leak" is real but inert. It costs RSS until
|
|
||||||
clobbered; it doesn't cost correctness. That's exactly
|
|
||||||
the property the forkserver pattern is built on, and
|
|
||||||
it's also why the design needs the "calling thread is
|
|
||||||
trio-free" precondition to be airtight: if the survivor
|
|
||||||
were a trio thread, it *would* try to drive the leaked
|
|
||||||
trio state, and the leak would no longer be inert.
|
|
||||||
|
|
||||||
## See also
|
|
||||||
|
|
||||||
- `tractor/spawn/_main_thread_forkserver.py` — module
|
|
||||||
docstring's "What survives the fork? — POSIX
|
|
||||||
semantics" section is the in-tree, code-adjacent
|
|
||||||
prose this doc expands on. The cross-table here is a
|
|
||||||
fourth-column expansion of the table there.
|
|
||||||
|
|
||||||
- [python-trio/trio#1614][trio-1614] — the trio issue
|
|
||||||
with the "leaked" framing, and the canonical thread
|
|
||||||
for trio + `fork()` hazards more broadly.
|
|
||||||
|
|
||||||
- [`subint_fork_blocked_by_cpython_post_fork_issue.md`](./subint_fork_blocked_by_cpython_post_fork_issue.md)
|
|
||||||
— sibling analysis covering CPython's *post-fork*
|
|
||||||
hooks (`PyOS_AfterFork_Child`,
|
|
||||||
`_PyInterpreterState_DeleteExceptMain`) and why
|
|
||||||
fork-from-non-main-subint is a CPython-level hard
|
|
||||||
refusal. Complementary axis: this doc is about POSIX
|
|
||||||
semantics; that doc is about the CPython runtime
|
|
||||||
layer that runs *after* POSIX `fork()` returns in
|
|
||||||
the child.
|
|
||||||
|
|
||||||
- `man pthread_atfork(3)` — canonical "fork in a
|
|
||||||
multithreaded process is dangerous" reference.
|
|
||||||
Especially the rationale section, which is the
|
|
||||||
closest thing to a normative statement of "the
|
|
||||||
surviving thread cannot safely use anything the dead
|
|
||||||
threads were touching."
|
|
||||||
|
|
||||||
- `man fork(2)` (Linux) — "Other than [the calling
|
|
||||||
thread], … no other threads are replicated …"
|
|
||||||
paragraph is the kernel-side statement of the
|
|
||||||
execution-side framing this doc opens with.
|
|
||||||
|
|
||||||
[trio-1614]: https://github.com/python-trio/trio/issues/1614
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
# Spawn-time boot-death (`rc=2`) under rapid same-name spawn against a registrar
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
Spawning N (≥4) sub-actors with the **same name** in tight
|
|
||||||
succession against a daemon registrar surfaces as
|
|
||||||
`ActorFailure: Sub-actor (...) died during boot (rc=2)
|
|
||||||
before completing parent-handshake`.
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/discovery/test_multi_program.py
|
|
||||||
::test_dup_name_cancel_cascade_escalates_to_hard_kill[n_dups=4]
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
tractor._exceptions.ActorFailure:
|
|
||||||
Sub-actor ('doggy', '<uuid>') died during boot (rc=2)
|
|
||||||
before completing parent-handshake.
|
|
||||||
proc: <_ForkedProc pid=<n> returncode=None>
|
|
||||||
```
|
|
||||||
|
|
||||||
The `proc` repr shows `returncode=None` because the repr is
|
|
||||||
captured before `proc.wait()` returns; the actual
|
|
||||||
`os.WEXITSTATUS == 2` is reported via `result['died']` in the
|
|
||||||
race-helper.
|
|
||||||
|
|
||||||
## When it surfaces
|
|
||||||
|
|
||||||
- N=2 (`n_dups=2`): **always passes**.
|
|
||||||
- N=4 (`n_dups=4`): **consistent fail** under both `tpt-proto=tcp`
|
|
||||||
and `tpt-proto=uds`, MTF backend.
|
|
||||||
- N=8 (`n_dups=8`): **passes** (counter-intuitive — see "racing
|
|
||||||
windows").
|
|
||||||
- Non-MTF backends: not yet exercised systematically.
|
|
||||||
|
|
||||||
## What previously masked it
|
|
||||||
|
|
||||||
Pre the spawn-time `wait_for_peer_or_proc_death` race-helper
|
|
||||||
(in `tractor.spawn._spawn`), the parent's `start_actor` flow
|
|
||||||
ended with a bare:
|
|
||||||
|
|
||||||
```python
|
|
||||||
event, chan = await ipc_server.wait_for_peer(uid)
|
|
||||||
```
|
|
||||||
|
|
||||||
That awaits an unsignalled `trio.Event` on `_peer_connected[uid]`.
|
|
||||||
If the sub-actor process **dies during boot** (before its
|
|
||||||
runtime executes the parent-callback handshake that sets the
|
|
||||||
event), the wait parks forever. The dead proc becomes a zombie
|
|
||||||
because no one ever calls `proc.wait()` to reap it.
|
|
||||||
|
|
||||||
In test contexts the failure presented as a hang or a much
|
|
||||||
later `trio.TooSlowError` from an outer `fail_after`. In
|
|
||||||
production it'd present as a parent that never makes progress
|
|
||||||
past `start_actor`. The death itself was silently masked.
|
|
||||||
|
|
||||||
## What surfaces it now
|
|
||||||
|
|
||||||
`tractor.spawn._spawn.wait_for_peer_or_proc_death` (used by
|
|
||||||
`_main_thread_forkserver_proc`) races the handshake-wait
|
|
||||||
against `proc.wait()`. The race-helper raises `ActorFailure`
|
|
||||||
on death-first instead of parking, exposing the rc=2.
|
|
||||||
|
|
||||||
## Hypothesis: registrar-side same-name contention
|
|
||||||
|
|
||||||
The test spawns N actors with name `doggy` sequentially:
|
|
||||||
|
|
||||||
```python
|
|
||||||
for i in range(n_dups):
|
|
||||||
p: Portal = await an.start_actor('doggy')
|
|
||||||
portals.append(p)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each spawned doggy:
|
|
||||||
|
|
||||||
1. Forks via the forkserver.
|
|
||||||
2. Boots its runtime in `_actor_child_main`.
|
|
||||||
3. Connects back to the parent for handshake.
|
|
||||||
4. Connects to the daemon registrar to call `register_actor`.
|
|
||||||
5. Enters its RPC msg-loop.
|
|
||||||
|
|
||||||
Step (4) is where the same-name contention lives. The
|
|
||||||
registrar's `register_actor` (in
|
|
||||||
`tractor.discovery._registry`) accepts duplicate names
|
|
||||||
(stores `(name, uuid) -> addr`), but its internal bookkeeping
|
|
||||||
may have a non-trivial check (e.g. `wait_for_actor` resolution,
|
|
||||||
`_addrs2aids` map updates) that errors out under specific
|
|
||||||
ordering between the existing entry and the incoming one.
|
|
||||||
|
|
||||||
`rc=2 == os.WEXITSTATUS == 2` corresponds to `sys.exit(2)`
|
|
||||||
in the doggy process — typically reached via an unhandled
|
|
||||||
exception that's translated to exit code 2 by Python's top-
|
|
||||||
level (e.g. `argparse` errors use 2; `SystemExit(2)` etc.).
|
|
||||||
So the doggy is hitting an explicit exit path during
|
|
||||||
`register_actor` or just-after.
|
|
||||||
|
|
||||||
The non-monotonic shape (N=2 OK, N=4 BAD, N=8 OK) suggests a
|
|
||||||
specific timing window — likely "the 3rd register-RPC arrives
|
|
||||||
while the 1st-or-2nd is in some intermediate state". With
|
|
||||||
N=8, the additional procs widen the registration spread
|
|
||||||
enough that no two land in the conflicting window.
|
|
||||||
|
|
||||||
## Where to dig next
|
|
||||||
|
|
||||||
- Add per-actor logging in `_actor_child_main` and
|
|
||||||
`register_actor` to surface the actual exception that
|
|
||||||
triggers the rc=2 exit. Currently the doggy dies before
|
|
||||||
the parent ever sees its stderr (forkserver doesn't
|
|
||||||
marshal child stdio back).
|
|
||||||
- Race-test the registrar's `register_actor` /
|
|
||||||
`unregister_actor` / `wait_for_actor` against same-name
|
|
||||||
concurrent calls in isolation (no spawn).
|
|
||||||
- Consider whether `register_actor` should be idempotent
|
|
||||||
under same-name re-register or should explicitly reject
|
|
||||||
same-name (and ideally with a clear `RemoteActorError`,
|
|
||||||
not `sys.exit(2)`).
|
|
||||||
|
|
||||||
## Test-suite handling
|
|
||||||
|
|
||||||
Currently:
|
|
||||||
|
|
||||||
- `tests/discovery/test_multi_program.py
|
|
||||||
::test_dup_name_cancel_cascade_escalates_to_hard_kill[n_dups=4]`
|
|
||||||
is `pytest.mark.xfail(strict=False, reason=...)` to keep
|
|
||||||
the suite green while this issue is investigated.
|
|
||||||
- `n_dups=2` and `n_dups=8` continue to validate the
|
|
||||||
cancel-cascade hard-kill escalation.
|
|
||||||
|
|
||||||
Once the underlying race is understood + fixed, drop the
|
|
||||||
xfail.
|
|
||||||
|
|
||||||
## Related work
|
|
||||||
|
|
||||||
- The cancel-cascade fix that introduced this regression
|
|
||||||
test:
|
|
||||||
`tractor/_exceptions.py:ActorTooSlowError`,
|
|
||||||
`tractor/runtime/_supervise.py:_try_cancel_then_kill`,
|
|
||||||
`tractor/runtime/_portal.py:Portal.cancel_actor(
|
|
||||||
raise_on_timeout=...)`.
|
|
||||||
- The spawn-time death-detection that exposed this:
|
|
||||||
`tractor/spawn/_spawn.py:wait_for_peer_or_proc_death`,
|
|
||||||
used by `tractor/spawn/_main_thread_forkserver.py`.
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
# `test_register_duplicate_name` racy connect-failure on `daemon` fixture readiness
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
`tests/test_multi_program.py::test_register_duplicate_name`
|
|
||||||
fails intermittently under BOTH transports + ALL spawn
|
|
||||||
backends with connect-refused errors:
|
|
||||||
|
|
||||||
```
|
|
||||||
# under --tpt-proto=uds
|
|
||||||
FAILED tests/test_multi_program.py::test_register_duplicate_name
|
|
||||||
- ConnectionRefusedError: [Errno 111] Connection refused
|
|
||||||
( ^^^ this exc was collapsed from a group ^^^ )
|
|
||||||
|
|
||||||
# under --tpt-proto=tcp
|
|
||||||
FAILED tests/test_multi_program.py::test_register_duplicate_name
|
|
||||||
- OSError: all attempts to connect to 127.0.0.1:36003 failed
|
|
||||||
( ^^^ this exc was collapsed from a group ^^^ )
|
|
||||||
```
|
|
||||||
|
|
||||||
Distinct from the cancel-cascade `TooSlowError` flake
|
|
||||||
class — see
|
|
||||||
`cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`.
|
|
||||||
This is a **connect-time race** before the daemon is
|
|
||||||
fully ready to `accept()`, not a teardown-cascade
|
|
||||||
slowness.
|
|
||||||
|
|
||||||
## Root cause: blind `time.sleep()` in `daemon` fixture
|
|
||||||
|
|
||||||
`tests/conftest.py::daemon` boots a sub-py-process via
|
|
||||||
`subprocess.Popen([python, '-c', 'tractor.run_daemon(...)'])`,
|
|
||||||
then **blindly sleeps** a fixed delay before yielding
|
|
||||||
`proc` to the test:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# excerpt from tests/conftest.py::daemon
|
|
||||||
proc = subprocess.Popen([
|
|
||||||
sys.executable, '-c', code,
|
|
||||||
])
|
|
||||||
|
|
||||||
bg_daemon_spawn_delay: float = _PROC_SPAWN_WAIT # 0.6
|
|
||||||
if tpt_proto == 'uds':
|
|
||||||
bg_daemon_spawn_delay += 1.6
|
|
||||||
if _non_linux and ci_env:
|
|
||||||
bg_daemon_spawn_delay += 1
|
|
||||||
|
|
||||||
# XXX, allow time for the sub-py-proc to boot up.
|
|
||||||
# !TODO, see ping-polling ideas above!
|
|
||||||
time.sleep(bg_daemon_spawn_delay)
|
|
||||||
|
|
||||||
assert not proc.returncode
|
|
||||||
yield proc
|
|
||||||
```
|
|
||||||
|
|
||||||
Inherent fragility: the delay is "long enough on dev
|
|
||||||
boxes most of the time" but has no actual
|
|
||||||
synchronization with the daemon's `bind()` + `listen()`
|
|
||||||
completion. Under any of:
|
|
||||||
|
|
||||||
- Loaded box (CI parallelism, big rebuild in
|
|
||||||
background, low-cpu-freq)
|
|
||||||
- Cold first-run (`importlib` cache miss, JIT warmup)
|
|
||||||
- Higher-than-expected `tractor` import cost
|
|
||||||
- Filesystem latency (UDS sockfile create, slow
|
|
||||||
tmpfs)
|
|
||||||
|
|
||||||
...the sleep finishes BEFORE the daemon has bound its
|
|
||||||
listen socket → first test client call to
|
|
||||||
`tractor.find_actor()` / `wait_for_actor()` /
|
|
||||||
`open_nursery(registry_addrs=[reg_addr])`'s implicit
|
|
||||||
connect → `ConnectionRefusedError` (TCP) or
|
|
||||||
`FileNotFoundError`/`ConnectionRefusedError` (UDS).
|
|
||||||
|
|
||||||
## Reproducer
|
|
||||||
|
|
||||||
Easiest: run the suite under load.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# create CPU pressure on another core in parallel
|
|
||||||
stress-ng --cpu 2 --timeout 600s &
|
|
||||||
|
|
||||||
./py313/bin/python -m pytest \
|
|
||||||
tests/test_multi_program.py::test_register_duplicate_name \
|
|
||||||
--spawn-backend=main_thread_forkserver \
|
|
||||||
--tpt-proto=tcp -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Reproduces ~30-50% of the time on a dev laptop. On a
|
|
||||||
quiet idle box, may need 5-10 runs to hit.
|
|
||||||
|
|
||||||
## Why the existing `_PROC_SPAWN_WAIT` tuning is
|
|
||||||
inadequate
|
|
||||||
|
|
||||||
Recent `bg_daemon_spawn_delay` rename
|
|
||||||
(de-monotonic-grow fix) just-shipped removed the
|
|
||||||
*accumulation* bug where each invocation made the
|
|
||||||
NEXT test's wait longer too. Net effect: every
|
|
||||||
invocation now uses the SAME `0.6 + 1.6` (UDS) or
|
|
||||||
`0.6` (TCP) sleep, no growth. Good — but does
|
|
||||||
NOTHING for the underlying race. Each individual
|
|
||||||
test still relies on a blind sleep that may or may
|
|
||||||
not be sufficient.
|
|
||||||
|
|
||||||
Bumping the constant higher pushes flake rate down
|
|
||||||
but never to zero AND adds dead time to every
|
|
||||||
non-flaking run. Not a fix, just a knob.
|
|
||||||
|
|
||||||
## Side effects
|
|
||||||
|
|
||||||
- **Inter-test cascade**: a single failure can cascade
|
|
||||||
via leaked subprocesses (the `daemon` fixture's
|
|
||||||
cleanup may not fully tear down a daemon that never
|
|
||||||
reached "ready"). The `_reap_orphaned_subactors`
|
|
||||||
session-end + `_track_orphaned_uds_per_test`
|
|
||||||
per-test fixtures handle most of this now, but the
|
|
||||||
affected test itself still fails.
|
|
||||||
- **Worsens under fork-spawn backends**: the daemon
|
|
||||||
has more init work
|
|
||||||
(`_main_thread_forkserver`-coordinator-thread
|
|
||||||
startup, etc.) so the sleep has to cover MORE.
|
|
||||||
|
|
||||||
## Fix design — replace blind sleep with active poll
|
|
||||||
|
|
||||||
The right primitive is **poll the daemon's bind
|
|
||||||
address until it accepts a connection or we time
|
|
||||||
out**, with the timeout being a hard ceiling rather
|
|
||||||
than a baseline. Two implementation paths:
|
|
||||||
|
|
||||||
### Path A — TCP/UDS connect-poll loop
|
|
||||||
|
|
||||||
Try `socket.connect(reg_addr)` in a tight loop with
|
|
||||||
short backoff (~50ms), succeed on the first non-error
|
|
||||||
return, fail-loud on a hard cap (e.g. 10s). Same
|
|
||||||
primitive works for both transports because both use
|
|
||||||
`socket.connect()` semantics.
|
|
||||||
|
|
||||||
Rough shape:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _wait_for_daemon_ready(
|
|
||||||
reg_addr,
|
|
||||||
tpt_proto: str,
|
|
||||||
timeout: float = 10.0,
|
|
||||||
poll_interval: float = 0.05,
|
|
||||||
) -> None:
|
|
||||||
deadline = time.monotonic() + timeout
|
|
||||||
while True:
|
|
||||||
if tpt_proto == 'tcp':
|
|
||||||
sock = socket.socket(socket.AF_INET)
|
|
||||||
target = reg_addr # (host, port)
|
|
||||||
else: # uds
|
|
||||||
sock = socket.socket(socket.AF_UNIX)
|
|
||||||
target = os.path.join(*reg_addr)
|
|
||||||
try:
|
|
||||||
sock.settimeout(poll_interval)
|
|
||||||
sock.connect(target)
|
|
||||||
except (
|
|
||||||
ConnectionRefusedError,
|
|
||||||
FileNotFoundError,
|
|
||||||
socket.timeout,
|
|
||||||
) as exc:
|
|
||||||
if time.monotonic() >= deadline:
|
|
||||||
raise TimeoutError(
|
|
||||||
f'Daemon never accepted on {target!r} '
|
|
||||||
f'within {timeout}s'
|
|
||||||
) from exc
|
|
||||||
time.sleep(poll_interval)
|
|
||||||
else:
|
|
||||||
sock.close()
|
|
||||||
return
|
|
||||||
```
|
|
||||||
|
|
||||||
Pros: trivial primitive, no tractor-runtime
|
|
||||||
dependency, works pre-yield in the fixture body,
|
|
||||||
fail-fast on truly-broken daemon.
|
|
||||||
Cons: doesn't actually do an IPC handshake, just
|
|
||||||
proves listen-side is up. A daemon that bound but
|
|
||||||
hasn't initialized its registrar table yet would
|
|
||||||
still race.
|
|
||||||
|
|
||||||
### Path B — `tractor.find_actor()` poll
|
|
||||||
|
|
||||||
Use the actual discovery API the test would call:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def _wait_for_daemon_ready_via_discovery(
|
|
||||||
reg_addr,
|
|
||||||
timeout: float = 10.0,
|
|
||||||
poll_interval: float = 0.05,
|
|
||||||
):
|
|
||||||
deadline = trio.current_time() + timeout
|
|
||||||
async with tractor.open_root_actor(
|
|
||||||
registry_addrs=[reg_addr],
|
|
||||||
# ephemeral root just for the probe
|
|
||||||
):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
async with tractor.find_actor(
|
|
||||||
'registrar', # daemon's own name
|
|
||||||
registry_addrs=[reg_addr],
|
|
||||||
) as portal:
|
|
||||||
if portal is not None:
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if trio.current_time() >= deadline:
|
|
||||||
raise TimeoutError(...)
|
|
||||||
await trio.sleep(poll_interval)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pros: actually proves the discovery path works,
|
|
||||||
handles the "bound but not ready" case naturally.
|
|
||||||
Cons: requires booting an ephemeral root actor JUST
|
|
||||||
for the probe (overhead), more code, and runs in trio
|
|
||||||
which complicates the sync-fixture context. Need a
|
|
||||||
`trio.run()` wrapper.
|
|
||||||
|
|
||||||
### Recommended: Path A with optional handshake check
|
|
||||||
|
|
||||||
Path A is much simpler + handles 95% of the bug
|
|
||||||
class. If "bound-but-not-ready" turns out to still
|
|
||||||
race (it shouldn't — `tractor.run_daemon` doesn't
|
|
||||||
return from `bind()` until the registrar is
|
|
||||||
fully populated), escalate to Path B as a focused
|
|
||||||
follow-up.
|
|
||||||
|
|
||||||
## Workarounds (until fix lands)
|
|
||||||
|
|
||||||
1. **Bump `_PROC_SPAWN_WAIT`** higher (current: 0.6).
|
|
||||||
2.0–3.0 hides most flakes at the cost of adding
|
|
||||||
dead time to every test. Not a fix but reduces
|
|
||||||
blast radius while the proper poll lands.
|
|
||||||
2. **`pytest-rerunfailures`** with `reruns=1` on the
|
|
||||||
`daemon` fixture's tests specifically. Hides the
|
|
||||||
flake but doesn't address it.
|
|
||||||
3. **Mark known-affected tests as `xfail(strict=False)`**
|
|
||||||
under `--ci`. Lets CI go green at the cost of
|
|
||||||
silently hiding regressions.
|
|
||||||
|
|
||||||
(Recommend skipping all three — implement the active
|
|
||||||
poll instead.)
|
|
||||||
|
|
||||||
## Investigation next steps
|
|
||||||
|
|
||||||
1. Implement Path A as a `_wait_for_daemon_ready()`
|
|
||||||
helper in `tests/conftest.py`. Replace the
|
|
||||||
`time.sleep(bg_daemon_spawn_delay)` call with it.
|
|
||||||
2. Drop the `_PROC_SPAWN_WAIT` constant entirely
|
|
||||||
(active poll obsoletes blind sleep).
|
|
||||||
3. Run the suite 5-10 times to validate flake rate
|
|
||||||
drops to 0.
|
|
||||||
4. If flakes persist, profile whether the daemon
|
|
||||||
process exits with non-zero before the poll's
|
|
||||||
deadline hits — that'd be a different bug
|
|
||||||
(daemon startup crash) that the blind sleep was
|
|
||||||
masking.
|
|
||||||
5. Cross-check `tests/test_multi_program.py::test_*`
|
|
||||||
— multiple tests use the `daemon` fixture; all
|
|
||||||
should benefit from the same poll primitive.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `tests/conftest.py::daemon` — the fixture under
|
|
||||||
fix
|
|
||||||
- `tests/conftest.py::_PROC_SPAWN_WAIT` — the
|
|
||||||
constant to drop
|
|
||||||
- `cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`
|
|
||||||
— distinct flake class (cancel-cascade
|
|
||||||
`TooSlowError` at teardown, not connect-time race)
|
|
||||||
- `trio_wakeup_socketpair_busy_loop_under_fork_issue.md`
|
|
||||||
— different bug entirely; this race was masked
|
|
||||||
pre-WakeupSocketpair-patch by the busy-loop
|
|
||||||
hangs.
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
# `trio` 0.29 -> 0.33 slows the depth=3 cancel-cascade
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
After locking to `trio==0.33.0` (commit `c7741bba`, was
|
|
||||||
`0.29.0`), this test reliably trips its `fail_after`
|
|
||||||
deadline on the **`trio`** backend:
|
|
||||||
|
|
||||||
```
|
|
||||||
FAILED tests/test_cancellation.py::test_nested_multierrors[start_method=trio-depth=3]
|
|
||||||
- AssertionError: assert False
|
|
||||||
where False = isinstance(
|
|
||||||
Cancelled(source='deadline', source_task=None, reason=None),
|
|
||||||
tractor.RemoteActorError,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
A `fail_after_w_trace` hang-snapshot is captured for the
|
|
||||||
test each run (deadline-injected `Cancelled` wrapped into
|
|
||||||
the actor-nursery `BaseExceptionGroup`).
|
|
||||||
|
|
||||||
## Root cause (immediate)
|
|
||||||
|
|
||||||
The test budgets `fail_after(6)` for the `trio` backend.
|
|
||||||
That 6s was chosen (commit `32955db0`, while `trio==0.29`)
|
|
||||||
with the assertion that trio finishes "well under" 6s.
|
|
||||||
The `trio` 0.29 -> 0.33 bump slowed the depth=3 cascade
|
|
||||||
past that budget, so the 6s deadline now fires mid-cascade.
|
|
||||||
|
|
||||||
trio 0.33 added **cancel-reason tracking** — every
|
|
||||||
`Cancelled` now carries `(source=, reason=, source_task=)`.
|
|
||||||
The injected exc is `Cancelled(source='deadline')`, i.e.
|
|
||||||
trio itself naming our `fail_after(6)` scope as the cancel
|
|
||||||
origin. When that `Cancelled` collapses one branch of the
|
|
||||||
nursery BEG, the test's `isinstance(subexc,
|
|
||||||
RemoteActorError)` assertion fails. The healthy outcome is
|
|
||||||
`BEG = [RemoteActorError, RemoteActorError]`; the
|
|
||||||
`Cancelled` is purely an artifact of the deadline cutting
|
|
||||||
the cascade short.
|
|
||||||
|
|
||||||
## Measurements (standalone, this machine)
|
|
||||||
|
|
||||||
```
|
|
||||||
depth=1 trio ~3.15s PASS (keeps 6s budget)
|
|
||||||
depth=3 trio ~6.8-8.2s FAIL @ 6s (now bumped to 12s)
|
|
||||||
```
|
|
||||||
|
|
||||||
depth=1 still fits comfortably; only depth=3 (deeper
|
|
||||||
recursive spawn-and-error tree => more actors to reap)
|
|
||||||
exceeds the old budget. The ~2s/depth-level cost looks
|
|
||||||
like serialized per-actor reap / `terminate_after` waits.
|
|
||||||
|
|
||||||
## Mitigation applied
|
|
||||||
|
|
||||||
`test_nested_multierrors` now splits the `trio` budget:
|
|
||||||
|
|
||||||
```python
|
|
||||||
case ('trio', 1):
|
|
||||||
timeout = 6
|
|
||||||
case ('trio', 3):
|
|
||||||
timeout = 12 # was 6; see this doc
|
|
||||||
```
|
|
||||||
|
|
||||||
This stops the deadline from firing so the cascade
|
|
||||||
completes naturally to `[RAE, RAE]`.
|
|
||||||
|
|
||||||
## Also affected — same root cause, different test
|
|
||||||
|
|
||||||
`test_echoserver_detailed_mechanics[trio-raise_error=KeyboardInterrupt]`
|
|
||||||
(`tests/test_infected_asyncio.py`) tripped the *same*
|
|
||||||
slowdown via its much tighter `trio` budget of `1s`. The
|
|
||||||
single-aio-subactor teardown now takes ~1s, so the `1s`
|
|
||||||
`fail_after` raced the deadline (PASS at 0.99s / FAIL at
|
|
||||||
1.03s across back-to-back standalone runs). On a deadline-
|
|
||||||
fire the injected `Cancelled(source='deadline')` wraps the
|
|
||||||
mid-stream `KeyboardInterrupt` into a `BaseExceptionGroup`,
|
|
||||||
which is NOT a `KeyboardInterrupt` so the bare
|
|
||||||
`pytest.raises(KeyboardInterrupt)` fails. (The sibling
|
|
||||||
`raise_error=Exception` variant only "passes" by accident:
|
|
||||||
an `ExceptionGroup` *is-a* `Exception`, so its
|
|
||||||
`pytest.raises(Exception)` still matches even when wrapped.)
|
|
||||||
|
|
||||||
Mitigation: bump that `trio` budget `1 -> 4s` (matching the
|
|
||||||
forking-spawner case). Without a deadline-fire the KBI
|
|
||||||
propagates bare and the assertion passes.
|
|
||||||
|
|
||||||
## Open follow-up (the actual regression)
|
|
||||||
|
|
||||||
The budget bump is a band-aid — the underlying question is
|
|
||||||
**why** the depth=3 `trio` cancel-cascade went from <6s to
|
|
||||||
~7-8s across `trio` 0.29 -> 0.33. Candidate avenues:
|
|
||||||
|
|
||||||
- which scope owns the per-actor `terminate_after` wait,
|
|
||||||
and are the tree's reaps concurrent or serialized?
|
|
||||||
- did trio 0.33's abort/reschedule or cancel-reason
|
|
||||||
bookkeeping change checkpoint timing on the cancel path?
|
|
||||||
|
|
||||||
If/when the cascade speeds back up under-budget, depth=3
|
|
||||||
will start completing well under 12s — at which point the
|
|
||||||
budget can be tightened back toward 6s as a regression
|
|
||||||
tripwire. Related (different backend, same cascade class):
|
|
||||||
`cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`.
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
# trio `WakeupSocketpair.drain()` busy-loop in forked child (peer-closed missed-EOF)
|
|
||||||
|
|
||||||
## Reproducer
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./py313/bin/python -m pytest \
|
|
||||||
tests/test_multi_program.py::test_register_duplicate_name \
|
|
||||||
--tpt-proto=tcp \
|
|
||||||
--spawn-backend=main_thread_forkserver \
|
|
||||||
-v --capture=sys
|
|
||||||
```
|
|
||||||
|
|
||||||
Subactor pegs a CPU core indefinitely; parent test
|
|
||||||
hangs waiting for the subactor.
|
|
||||||
|
|
||||||
## Empirical evidence (caught alive)
|
|
||||||
|
|
||||||
```
|
|
||||||
$ sudo strace -p <subactor-pid>
|
|
||||||
recvfrom(6, "", 65536, 0, NULL, NULL) = 0
|
|
||||||
recvfrom(6, "", 65536, 0, NULL, NULL) = 0
|
|
||||||
recvfrom(6, "", 65536, 0, NULL, NULL) = 0
|
|
||||||
... (no `epoll_wait`, no other syscalls, just this back-to-back)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pattern: tight C-level `recvfrom` loop returning 0
|
|
||||||
each call. No `epoll_wait` between iterations →
|
|
||||||
**not trio's task scheduler**. Pure synchronous C
|
|
||||||
loop.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ sudo readlink /proc/<subactor-pid>/fd/6
|
|
||||||
socket:[<inode>]
|
|
||||||
|
|
||||||
$ sudo lsof -p <subactor-pid> | grep ' 6u'
|
|
||||||
<cmd> <pid> goodboy 6u unix 0xffff... 0t0 <inode> type=STREAM (CONNECTED)
|
|
||||||
```
|
|
||||||
|
|
||||||
fd=6 is an **AF_UNIX socket** in CONNECTED state.
|
|
||||||
Even though the test uses `--tpt-proto=tcp`, this fd
|
|
||||||
is NOT a tractor IPC channel — it's an internal
|
|
||||||
trio socketpair.
|
|
||||||
|
|
||||||
## Root-cause: `WakeupSocketpair.drain()`
|
|
||||||
|
|
||||||
`/site-packages/trio/_core/_wakeup_socketpair.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class WakeupSocketpair:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.wakeup_sock, self.write_sock = socket.socketpair()
|
|
||||||
self.wakeup_sock.setblocking(False)
|
|
||||||
self.write_sock.setblocking(False)
|
|
||||||
...
|
|
||||||
|
|
||||||
def drain(self) -> None:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
self.wakeup_sock.recv(2**16)
|
|
||||||
except BlockingIOError:
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
`socket.socketpair()` on Linux defaults to AF_UNIX
|
|
||||||
SOCK_STREAM. Both ends non-blocking. Normal flow:
|
|
||||||
|
|
||||||
1. Signal/wake event → `write_sock.send(b'\x00')`
|
|
||||||
queues a byte.
|
|
||||||
2. `wakeup_sock` becomes readable → trio's epoll
|
|
||||||
triggers.
|
|
||||||
3. Trio calls `drain()` to flush the buffer.
|
|
||||||
4. drain loops on `wakeup_sock.recv(64KB)`.
|
|
||||||
5. Eventually buffer empty → non-blocking socket
|
|
||||||
raises `BlockingIOError` → except → break.
|
|
||||||
|
|
||||||
**Bug surface — peer-closed missed-EOF**:
|
|
||||||
|
|
||||||
Non-blocking socket semantics:
|
|
||||||
- buffer has data → `recv` returns N>0 bytes (loop continues)
|
|
||||||
- buffer empty → `recv` raises `BlockingIOError`
|
|
||||||
- **peer FIN'd → `recv` returns 0 bytes (NEITHER exception NOR
|
|
||||||
break — infinite tight loop)**
|
|
||||||
|
|
||||||
`drain()` does not handle the `b''` return-value
|
|
||||||
(EOF) case. If `write_sock` has been closed (or the
|
|
||||||
process holding it is gone), every iteration returns
|
|
||||||
0 → infinite loop → 100% CPU on a single core.
|
|
||||||
|
|
||||||
## Why this triggers under `main_thread_forkserver`
|
|
||||||
|
|
||||||
Under `os.fork()` from the forkserver-worker thread:
|
|
||||||
|
|
||||||
1. Parent has a `WakeupSocketpair` instance with
|
|
||||||
`wakeup_sock=fdN`, `write_sock=fdM`. Both fds
|
|
||||||
open in parent.
|
|
||||||
2. Fork → child inherits BOTH fds (kernel-level fd
|
|
||||||
table dup).
|
|
||||||
3. `_close_inherited_fds()` runs in child →
|
|
||||||
closes everything except stdio. `wakeup_sock` and
|
|
||||||
`write_sock` of the parent's `WakeupSocketpair`
|
|
||||||
ARE closed in child.
|
|
||||||
4. Child's trio (running fresh) creates its OWN
|
|
||||||
`WakeupSocketpair` → NEW fd numbers (e.g. fd 6, 7).
|
|
||||||
5. **In `infect_asyncio` mode** the asyncio loop is
|
|
||||||
the host; trio runs as guest via
|
|
||||||
`start_guest_run`. trio still creates its
|
|
||||||
`WakeupSocketpair` in the I/O manager but its
|
|
||||||
role is different.
|
|
||||||
|
|
||||||
The race window: somewhere between (3) and (5), if a
|
|
||||||
`WakeupSocketpair` Python object reference inherited
|
|
||||||
via COW (from parent's pre-fork heap) survives long
|
|
||||||
enough that `drain()` is called on it AFTER its fds
|
|
||||||
were closed but BEFORE the child's NEW socketpair
|
|
||||||
takes over the recycled fd numbers — the recycled fd
|
|
||||||
will be one of the child's NEW socketpair ends, whose
|
|
||||||
peer might be FIN-flagged (e.g. parent-process
|
|
||||||
peer-end is closed).
|
|
||||||
|
|
||||||
Or simpler: the `wait_for_actor`/`find_actor` discovery
|
|
||||||
flow in `test_register_duplicate_name` triggers an
|
|
||||||
unusual code path where a stale `WakeupSocketpair`
|
|
||||||
gets `drain()`-called on a fd whose peer has already
|
|
||||||
closed.
|
|
||||||
|
|
||||||
## Why `drain()` shouldn't loop indefinitely on EOF
|
|
||||||
(upstream trio bug)
|
|
||||||
|
|
||||||
Even WITHOUT fork, `drain()` should treat `b''` as
|
|
||||||
EOF and break. The current code is correct for the
|
|
||||||
"buffer drained on a healthy socketpair" scenario but
|
|
||||||
incorrect for the "peer is gone" scenario. It's a
|
|
||||||
defensive-programming gap in trio.
|
|
||||||
|
|
||||||
A one-line patch upstream:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def drain(self) -> None:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = self.wakeup_sock.recv(2**16)
|
|
||||||
if not data:
|
|
||||||
break # peer-closed; nothing more to drain
|
|
||||||
except BlockingIOError:
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workarounds (until the underlying issue lands)
|
|
||||||
|
|
||||||
1. **Skip-mark on the fork backend**:
|
|
||||||
`tests/test_multi_program.py` →
|
|
||||||
`pytest.mark.skipon_spawn_backend('main_thread_forkserver',
|
|
||||||
reason='trio WakeupSocketpair.drain busy-loop, see ai/conc-anal/trio_wakeup_socketpair_busy_loop_under_fork_issue.md')`.
|
|
||||||
|
|
||||||
2. **Defensive monkey-patch in tractor's
|
|
||||||
forkserver-child prelude** — wrap
|
|
||||||
`WakeupSocketpair.drain` to handle `b''`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# in `_actor_child_main` or `_close_inherited_fds`'s
|
|
||||||
# post-fork prelude:
|
|
||||||
from trio._core._wakeup_socketpair import WakeupSocketpair
|
|
||||||
_orig_drain = WakeupSocketpair.drain
|
|
||||||
def _safe_drain(self):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = self.wakeup_sock.recv(2**16)
|
|
||||||
if not data:
|
|
||||||
return # peer closed
|
|
||||||
except BlockingIOError:
|
|
||||||
pass
|
|
||||||
WakeupSocketpair.drain = _safe_drain
|
|
||||||
```
|
|
||||||
|
|
||||||
Tracks upstream — remove once trio fixes.
|
|
||||||
|
|
||||||
3. **Upstream the fix**: 1-line PR to `python-trio/trio`
|
|
||||||
adding `if not data: break` to `drain()`.
|
|
||||||
|
|
||||||
## Investigation next steps
|
|
||||||
|
|
||||||
1. **Confirm via py-spy**: when caught alive, detach
|
|
||||||
strace first then
|
|
||||||
`sudo py-spy dump --pid <subactor> --locals`. The
|
|
||||||
busy thread should show `drain` from `WakeupSocketpair`
|
|
||||||
in the call chain.
|
|
||||||
2. **Identify which write-end peer is closed**: from
|
|
||||||
the inode of fd 6, look up the matching peer
|
|
||||||
inode via `ss -xp` and see whose process it
|
|
||||||
was/is.
|
|
||||||
3. **Verify the missed-EOF hypothesis**: hand-craft a
|
|
||||||
minimal `WakeupSocketpair` repro:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from trio._core._wakeup_socketpair import WakeupSocketpair
|
|
||||||
ws = WakeupSocketpair()
|
|
||||||
ws.write_sock.close() # simulate peer-gone
|
|
||||||
ws.drain() # should hang forever
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sibling bug
|
|
||||||
|
|
||||||
`tests/test_infected_asyncio.py::test_aio_simple_error`
|
|
||||||
hangs under the same backend with a DIFFERENT
|
|
||||||
fingerprint (Mode-A deadlock, both parties in
|
|
||||||
`epoll_wait`, no busy-loop). Distinct root cause —
|
|
||||||
see `infected_asyncio_under_main_thread_forkserver_hang_issue.md`.
|
|
||||||
|
|
||||||
Both share the broader theme: **trio internal-state
|
|
||||||
initialization isn't fully fork-safe under
|
|
||||||
`main_thread_forkserver`** for the more exotic
|
|
||||||
dispatch paths.
|
|
||||||
|
|
||||||
## See also
|
|
||||||
|
|
||||||
- [#379](https://github.com/goodboy/tractor/issues/379) — subint umbrella
|
|
||||||
- python-trio/trio#1614 — trio + fork hazards
|
|
||||||
- `trio._core._wakeup_socketpair.WakeupSocketpair`
|
|
||||||
source (the smoking gun)
|
|
||||||
- `ai/conc-anal/fork_thread_semantics_execution_vs_memory.md`
|
|
||||||
- `ai/conc-anal/infected_asyncio_under_main_thread_forkserver_hang_issue.md`
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: (ad-hoc, not tracked via conf.toml)
|
|
||||||
timestamp: 2026-04-06T17:28:48Z
|
|
||||||
git_ref: 02b2ef1
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260406T172848Z_02b2ef1_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
User asked to extend `tests/test_resource_cache.py` with a test
|
|
||||||
that reproduces the edge case fixed in commit `02b2ef18` (per-key
|
|
||||||
locking+user tracking in `maybe_open_context()`). The bug was
|
|
||||||
originally triggered in piker's `brokerd.kraken` backend where the
|
|
||||||
same `acm_func` was called with different kwargs, and the old
|
|
||||||
global `_Cache.users` counter caused:
|
|
||||||
|
|
||||||
- teardown skipped for one `ctx_key` bc another key's users kept
|
|
||||||
the global count > 0
|
|
||||||
- re-entry hitting `assert not resources.get(ctx_key)` during the
|
|
||||||
teardown window
|
|
||||||
|
|
||||||
User requested a test that would fail under the old code and pass
|
|
||||||
with the fix.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Designed and implemented `test_per_ctx_key_resource_lifecycle`
|
|
||||||
which verifies per-`ctx_key` resource isolation by:
|
|
||||||
|
|
||||||
1. Holding resource `'a'` open in a bg task
|
|
||||||
2. Opening+closing resource `'b'` (same `acm_func`, different
|
|
||||||
kwargs) while `'a'` is still alive
|
|
||||||
3. Re-opening `'b'` and asserting cache MISS — proving `'b'` was
|
|
||||||
torn down independently despite `'a'` keeping its own user
|
|
||||||
count > 0
|
|
||||||
|
|
||||||
With the old global counter, phase 3 would produce a stale cache
|
|
||||||
HIT (leaked resource) or crash on the assert.
|
|
||||||
|
|
||||||
Also added a trivial `acm_with_resource(resource_id)` ACM helper
|
|
||||||
at module level.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/test_resource_cache.py` — add `acm_with_resource` ACM +
|
|
||||||
`test_per_ctx_key_resource_lifecycle` test fn
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated (pending user review).
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: (ad-hoc, not tracked via conf.toml)
|
|
||||||
timestamp: 2026-04-06T19:31:25Z
|
|
||||||
git_ref: 85f9c5d
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260406T193125Z_85f9c5d_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
User asked to reproduce the exact `assert not resources.get(ctx_key)`
|
|
||||||
crash originally seen in piker's `brokerd.kraken` backend via
|
|
||||||
`open_cached_client('kraken')`. Key constraints from user:
|
|
||||||
|
|
||||||
- In piker, kwargs were the **same** (empty) so all callers
|
|
||||||
share one `ctx_key = (fid, ())`
|
|
||||||
- The root issue is `_Cache.locks` being indexed by `fid`
|
|
||||||
rather than `ctx_key`, plus the race window between
|
|
||||||
`values.pop()` and `resources.pop()` in `_Cache.run_ctx`
|
|
||||||
(the acm `__aexit__` has checkpoints in between)
|
|
||||||
- Mark the test `@pytest.mark.xfail` so it can be committed
|
|
||||||
against the unpatched code and later adjusted once the fix
|
|
||||||
from `9e49eddd` is applied
|
|
||||||
|
|
||||||
User pointed to piker's `open_cached_client('kraken')` usage
|
|
||||||
as the real-world pattern to model.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Added `test_moc_reentry_during_teardown` which deterministically
|
|
||||||
reproduces the teardown race:
|
|
||||||
|
|
||||||
1. A `cached_client()` acm (no params, like
|
|
||||||
`kraken.api.get_client()`) signals via `in_aexit` event
|
|
||||||
when its `__aexit__` starts, then sleeps to hold the
|
|
||||||
window open
|
|
||||||
2. `use_and_exit` task enters+exits the cached resource
|
|
||||||
3. `reenter_during_teardown` waits on `in_aexit` then
|
|
||||||
re-enters — landing exactly in the window where
|
|
||||||
`values` is popped but `resources` still exists
|
|
||||||
4. The old `assert not resources.get(ctx_key)` fires
|
|
||||||
5. `@pytest.mark.xfail(raises=AssertionError)` catches it
|
|
||||||
|
|
||||||
Test confirmed: `XFAIL` with the exact traceback at
|
|
||||||
`_mngrs.py:327`.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/test_resource_cache.py` — add
|
|
||||||
`test_moc_reentry_during_teardown` xfail test fn
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated (pending user review).
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: (ad-hoc, not tracked via conf.toml)
|
|
||||||
timestamp: 2026-04-06T22:26:48Z
|
|
||||||
git_ref: f086222
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260406T222648Z_f086222_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
User applied the per-key locking patch (`f086222d`),
|
|
||||||
removed the `@pytest.mark.xfail` from
|
|
||||||
`test_moc_reentry_during_teardown`, and found the
|
|
||||||
test STILL fails — now with `RuntimeError` instead
|
|
||||||
of `AssertionError`. Asked to diagnose why the fix
|
|
||||||
doesn't actually resolve this race.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Traced the exact scheduling sequence across three
|
|
||||||
concurrent tasks to show that the per-`ctx_key`
|
|
||||||
locking change doesn't close the fundamental gap:
|
|
||||||
|
|
||||||
- `maybe_open_context`'s finally pops the lock and
|
|
||||||
sets `no_more_users` in the **exiting caller's
|
|
||||||
task**
|
|
||||||
- But the actual resource cleanup (`values.pop`,
|
|
||||||
acm `__aexit__`, `resources.pop`) runs in the
|
|
||||||
**`run_ctx` task** inside `service_tn`
|
|
||||||
- A re-entering task creates a fresh lock (old one
|
|
||||||
popped), acquires immediately, and hits stale
|
|
||||||
`resources[ctx_key]` while `run_ctx` is still in
|
|
||||||
`__aexit__`
|
|
||||||
|
|
||||||
Identified three possible fix strategies:
|
|
||||||
1. Let `run_ctx` own lock release/pop after
|
|
||||||
`resources.pop()`
|
|
||||||
2. Add a "teardown complete" event that new callers
|
|
||||||
wait on
|
|
||||||
3. Move `resources.pop()` into `run_ctx`'s inner
|
|
||||||
finally (before acm `__aexit__`)
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
No code changes — analysis only.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
N/A — diagnostic output, no code generated.
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-04-06T22:26:48Z
|
|
||||||
git_ref: f086222
|
|
||||||
diff_cmd: git diff f086222~1..f086222
|
|
||||||
---
|
|
||||||
|
|
||||||
Here's the problem. The per-key locking doesn't fix this race because the **lock lifetime is decoupled from `run_ctx`'s lifetime**. Trace through what happens:
|
|
||||||
|
|
||||||
**`maybe_open_context` finally** (lines 468-495):
|
|
||||||
1. `users[ctx_key] -= 1` → 0
|
|
||||||
2. `no_more_users.set()` → wakes `run_ctx` (but it doesn't run yet, just becomes ready)
|
|
||||||
3. `_Cache.locks.pop(ctx_key)` → **lock is gone**
|
|
||||||
4. `maybe_open_context` exits
|
|
||||||
|
|
||||||
**`run_ctx`** now gets scheduled (lines 237-241):
|
|
||||||
5. Inner finally: `values.pop(ctx_key)` → values gone
|
|
||||||
6. `mng.__aexit__()` runs → `in_aexit.set()`, `await trio.sleep(10)` → **CHECKPOINT**
|
|
||||||
|
|
||||||
**`reenter_during_teardown`** wakes:
|
|
||||||
7. Enters `maybe_open_context` → no lock exists for `ctx_key` → creates **fresh** lock, acquires immediately
|
|
||||||
8. `values[ctx_key]` → `KeyError`
|
|
||||||
9. `resources.get(ctx_key)` → **still exists!** (step 6 hasn't reached `resources.pop()` in `run_ctx`'s outer finally yet)
|
|
||||||
10. `raise RuntimeError('Caching resources ALREADY exist?!')`
|
|
||||||
|
|
||||||
The core issue: `no_more_users.set()` (step 2) and `locks.pop()` (step 3) happen in the **exiting caller's task**, but the actual resource cleanup (`values.pop`, acm `__aexit__`, `resources.pop`) happens in the **`run_ctx` task** inside `service_tn`. There's no synchronization between "lock removed, new callers welcome" and "run_ctx has actually finished tearing down". The per-`ctx_key` lock change just made the lock more granular — it didn't close this gap.
|
|
||||||
|
|
||||||
The fix needs to ensure a new caller can't acquire a lock for `ctx_key` until `run_ctx` has fully exited (including `resources.pop()`). Either:
|
|
||||||
- **don't pop the lock in `maybe_open_context`** — let `run_ctx` release/pop it after `resources.pop()`
|
|
||||||
- **wait on a "teardown complete" event** in the cache-miss path when stale `resources` exist
|
|
||||||
- **pop `resources` before the acm `__aexit__`** (move `resources.pop()` into the inner finally alongside `values.pop()`)
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: (ad-hoc, not tracked via conf.toml)
|
|
||||||
timestamp: 2026-04-13T19:21:16Z
|
|
||||||
git_ref: f851f28
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
User provided a detailed implementation plan for
|
|
||||||
a new `test_tpt_bind_addrs.py` test suite covering
|
|
||||||
`open_root_actor(tpt_bind_addrs=...)` — the three
|
|
||||||
runtime code paths in `_root.py:385-450`:
|
|
||||||
|
|
||||||
1. Non-registrar, no explicit bind -> random addrs
|
|
||||||
2. Registrar, no explicit bind -> registry_addrs
|
|
||||||
3. Explicit bind given -> `wrap_address()` + merge
|
|
||||||
|
|
||||||
Plan specified 6 test functions (~10 parametrized
|
|
||||||
variants), predicted a type-mixing bug at line 446,
|
|
||||||
and asked for an in-flight fix if confirmed.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Created `tests/discovery/test_tpt_bind_addrs.py`
|
|
||||||
with 9 collected test variants across 6 functions:
|
|
||||||
|
|
||||||
- `test_registrar_root_tpt_bind_addrs` (3 variants:
|
|
||||||
`bind-eq-reg`, `bind-subset-reg`,
|
|
||||||
`bind-disjoint-reg`)
|
|
||||||
- `test_non_registrar_root_tpt_bind_addrs`
|
|
||||||
(2 variants: `bind-same-bindspace`,
|
|
||||||
`bind-disjoint`)
|
|
||||||
- `test_non_registrar_default_random_bind`
|
|
||||||
(baseline, no explicit bind)
|
|
||||||
- `test_tpt_bind_addrs_as_maddr_str`
|
|
||||||
(multiaddr string input)
|
|
||||||
- `test_registrar_merge_binds_union`
|
|
||||||
(registrar + disjoint bind -> union)
|
|
||||||
- `test_open_nursery_forwards_tpt_bind_addrs`
|
|
||||||
(`open_nursery(**kwargs)` forwarding)
|
|
||||||
|
|
||||||
Confirmed and fixed the predicted bug at
|
|
||||||
`_root.py:446`: the registrar merge path mixed
|
|
||||||
`Address` objects (`tpt_bind_addrs`) with raw tuples
|
|
||||||
(`uw_reg_addrs`) inside `set()`, preventing
|
|
||||||
deduplication and causing double-bind `OSError`.
|
|
||||||
|
|
||||||
Fix: wrap `uw_reg_addrs` before the set union:
|
|
||||||
```python
|
|
||||||
# before (broken)
|
|
||||||
tpt_bind_addrs = list(set(
|
|
||||||
tpt_bind_addrs + uw_reg_addrs
|
|
||||||
))
|
|
||||||
# after (fixed)
|
|
||||||
tpt_bind_addrs = list(set(
|
|
||||||
tpt_bind_addrs
|
|
||||||
+ [wrap_address(a) for a in uw_reg_addrs]
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
All 9 tests pass after the fix.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/discovery/test_tpt_bind_addrs.py` (new)
|
|
||||||
- `tractor/_root.py:446` (bug fix, 1 line)
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
N/A — pending review.
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: 76154e65-d8e1-4b5f-9275-0ea45ba7e98a
|
|
||||||
timestamp: 2026-04-13T20:50:48Z
|
|
||||||
git_ref: 269d939c
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260413T205048Z_269d939c_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Implement a `parse_endpoints()` API in
|
|
||||||
`tractor.discovery._multiaddr` that lets downstream
|
|
||||||
projects declare per-actor transport bind addresses
|
|
||||||
as a service table of actor-name -> multiaddr
|
|
||||||
strings (loaded from config, e.g. TOML `[network]`
|
|
||||||
section). Add type aliases `EndpointsTable` and
|
|
||||||
`ParsedEndpoints`, plus 7 unit tests covering TCP,
|
|
||||||
UDS, tuples, mixed input, unsupported protocols, and
|
|
||||||
edge cases.
|
|
||||||
|
|
||||||
A detailed implementation plan was provided
|
|
||||||
specifying insertion points, type signatures, test
|
|
||||||
names, and verification commands.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Add `parse_endpoints()` function with two supporting
|
|
||||||
type aliases to `_multiaddr.py`. The function
|
|
||||||
iterates a `dict[str, list[str|tuple]]` service
|
|
||||||
table and delegates each entry to the existing
|
|
||||||
`wrap_address()` helper, which already handles maddr
|
|
||||||
strings, raw tuples, and pre-wrapped `Address`
|
|
||||||
objects. Seven pure unit tests (no trio runtime)
|
|
||||||
validate all input variants and edge cases. All 22
|
|
||||||
multiaddr tests pass.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/discovery/_multiaddr.py` — add
|
|
||||||
`EndpointsTable`, `ParsedEndpoints` type aliases
|
|
||||||
and `parse_endpoints()` function after line 133
|
|
||||||
- `tests/discovery/test_multiaddr.py` — add
|
|
||||||
`parse_endpoints` import and 7 new test functions
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated.
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-04-13T20:50:48Z
|
|
||||||
git_ref: 269d939c
|
|
||||||
diff_cmd: git diff 269d939c~1..269d939c
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation output
|
|
||||||
|
|
||||||
### `tractor/discovery/_multiaddr.py`
|
|
||||||
|
|
||||||
> `git diff 269d939c~1..269d939c -- tractor/discovery/_multiaddr.py`
|
|
||||||
|
|
||||||
Type aliases `EndpointsTable` and `ParsedEndpoints`
|
|
||||||
plus `parse_endpoints()` helper that wraps each
|
|
||||||
entry via `wrap_address()`.
|
|
||||||
|
|
||||||
### `tests/discovery/test_multiaddr.py`
|
|
||||||
|
|
||||||
> `git diff 269d939c~1..269d939c -- tests/discovery/test_multiaddr.py`
|
|
||||||
|
|
||||||
Add import of `parse_endpoints` to existing import
|
|
||||||
block, then 7 new test functions.
|
|
||||||
|
|
||||||
### Test run output
|
|
||||||
|
|
||||||
```
|
|
||||||
22 passed, 1 warning in 0.05s
|
|
||||||
```
|
|
||||||
|
|
||||||
All 22 tests pass (15 existing + 7 new).
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
session: multiaddr-support-rename-prefer
|
|
||||||
timestamp: 2026-04-14T16:33:00Z
|
|
||||||
git_ref: befedc49
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260414T163300Z_befedc49_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Create a helper function that determines the best transport given
|
|
||||||
actor locality (distributed vs same host). Use PID/hostname
|
|
||||||
comparison for locality detection, apply at registry addr selection
|
|
||||||
only (not spawn-time).
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
New `prefer_addr()` + `_is_local_addr()` helpers
|
|
||||||
in `_api.py` using `socket.getaddrinfo()` and
|
|
||||||
`ipaddress` for PID/hostname locality detection.
|
|
||||||
Preference: UDS > local TCP > remote TCP.
|
|
||||||
Integrated into `query_actor()` and
|
|
||||||
`wait_for_actor()`. Also changed
|
|
||||||
`Registrar.find_actor()` to return full addr list
|
|
||||||
so callers can apply preference.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/discovery/_discovery.py` → `_api.py`
|
|
||||||
— renamed + added `prefer_addr()`,
|
|
||||||
`_is_local_addr()`; updated `query_actor()` and
|
|
||||||
`wait_for_actor()` call sites
|
|
||||||
- `tractor/discovery/_registry.py`
|
|
||||||
— `Registrar.find_actor()` returns
|
|
||||||
`list[UnwrappedAddress]|None`
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-6
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-04-14T16:33:00Z
|
|
||||||
git_ref: befedc49
|
|
||||||
diff_cmd: git diff befedc49~1..befedc49
|
|
||||||
---
|
|
||||||
|
|
||||||
### `tractor/discovery/_api.py`
|
|
||||||
|
|
||||||
> `git diff befedc49~1..befedc49 -- tractor/discovery/_api.py`
|
|
||||||
|
|
||||||
Add `_is_local_addr()` and `prefer_addr()` transport
|
|
||||||
preference helpers.
|
|
||||||
|
|
||||||
#### `_is_local_addr(addr: Address) -> bool`
|
|
||||||
|
|
||||||
Determines whether an `Address` is reachable on the
|
|
||||||
local host:
|
|
||||||
|
|
||||||
- `UDSAddress`: always returns `True`
|
|
||||||
(filesystem-bound, inherently local)
|
|
||||||
- `TCPAddress`: checks if `._host` is a loopback IP
|
|
||||||
via `ipaddress.ip_address().is_loopback`, then
|
|
||||||
falls back to comparing against the machine's own
|
|
||||||
interface IPs via
|
|
||||||
`socket.getaddrinfo(socket.gethostname(), None)`
|
|
||||||
|
|
||||||
#### `prefer_addr(addrs: list[UnwrappedAddress]) -> UnwrappedAddress`
|
|
||||||
|
|
||||||
Selects the "best" transport address from a
|
|
||||||
multihomed actor's address list. Wraps each
|
|
||||||
candidate via `wrap_address()` to get typed
|
|
||||||
`Address` objects, then classifies into three tiers:
|
|
||||||
|
|
||||||
1. **UDS** (same-host guaranteed, lowest overhead)
|
|
||||||
2. **TCP loopback / same-host IP** (local network)
|
|
||||||
3. **TCP remote** (only option for distributed)
|
|
||||||
|
|
||||||
Within each tier, the last-registered (latest) entry
|
|
||||||
is preferred. Falls back to `addrs[-1]` if no
|
|
||||||
heuristic matches.
|
|
||||||
|
|
||||||
### `tractor/discovery/_registry.py`
|
|
||||||
|
|
||||||
> `git diff befedc49~1..befedc49 -- tractor/discovery/_registry.py`
|
|
||||||
|
|
||||||
`Registrar.find_actor()` return type broadened from
|
|
||||||
single addr to `list[UnwrappedAddress]|None` — full
|
|
||||||
addr list lets callers apply transport preference.
|
|
||||||
|
|
||||||
#### Integration
|
|
||||||
|
|
||||||
`query_actor()` and `wait_for_actor()` now call
|
|
||||||
`prefer_addr(addrs)` instead of `addrs[-1]`.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
|
|
||||||
All discovery tests pass (13/13 non-daemon).
|
|
||||||
`test_local.py` and `test_multi_program.py` also
|
|
||||||
pass (daemon fixture teardown failures are
|
|
||||||
pre-existing and unrelated).
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
session: subints-spawner-design-kickoff
|
|
||||||
timestamp: 2026-04-17T03:49:18Z
|
|
||||||
git_ref: 9703210
|
|
||||||
scope: docs
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260417T034918Z_9703210_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Drive the "first big boi, from GH issue" task seeded by
|
|
||||||
`ai/prompt-io/prompts/subints_spawner.md`: design, plan
|
|
||||||
and implement sub-interpreter (subint) spawn-backend
|
|
||||||
support per issue #379, including (1) modularizing
|
|
||||||
`tractor.spawn._spawn` into per-backend submods, (2) a new
|
|
||||||
`._subint` backend, and (3) harness parametrization via the
|
|
||||||
existing `--spawn-backend` / `start_method` pytest fixture
|
|
||||||
in `tractor._testing.pytest`.
|
|
||||||
|
|
||||||
Follow-up clarifications from the user (this turn):
|
|
||||||
1. Pin `<3.15` on this dev branch and feature-gate subint
|
|
||||||
tests — chose option (a).
|
|
||||||
2. Split Phase A (modularization) into its own PR first.
|
|
||||||
3. Defer the `fork()`-via-subint hack to a follow-up.
|
|
||||||
4. Harness flag is `pytest --spawn-backend <key>` CLI →
|
|
||||||
`start_method` fixture (prompt file updated to match).
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Produced a three-phase plan and a concrete Phase A (pure
|
|
||||||
modularization) file-split plan for user review; no code
|
|
||||||
written yet — the green-light to start Phase A was given
|
|
||||||
in this same turn conditional on logging this prompt-io
|
|
||||||
entry first.
|
|
||||||
|
|
||||||
Phases:
|
|
||||||
- **A — modularize** `tractor/spawn/_spawn.py` (847 LOC):
|
|
||||||
keep generic machinery in `_spawn.py`, extract
|
|
||||||
`trio_proc` → `spawn/_trio.py`, `mp_proc` →
|
|
||||||
`spawn/_mp.py`. No pin bump.
|
|
||||||
- **B — `_subint` backend**: bump `pyproject.toml`
|
|
||||||
`requires-python` upper to `<3.15`; add `'subint'` to
|
|
||||||
`SpawnMethodKey`; reuse existing UDS transport; shm
|
|
||||||
escape-hatch deferred.
|
|
||||||
- **C — harness**: drive the valid-backend tuple in
|
|
||||||
`tractor/_testing/pytest.py:345-349` from
|
|
||||||
`typing.get_args(SpawnMethodKey)`; skip subint tests on
|
|
||||||
Python < 3.14.
|
|
||||||
|
|
||||||
Key findings surfaced to the user:
|
|
||||||
- `pyproject.toml:12` currently pins `<3.14`; PEP 734
|
|
||||||
`concurrent.interpreters` only ships in 3.14 — the
|
|
||||||
load-bearing constraint.
|
|
||||||
- `_testing/pytest.py:345-349` hardcodes valid backends
|
|
||||||
as a string tuple (`'mp_spawn'`, `'mp_forkserver'`,
|
|
||||||
`'trio'`) — should be `get_args(SpawnMethodKey)`.
|
|
||||||
- `_testing/pytest.py:228` already imports
|
|
||||||
`try_set_start_method` from `tractor.spawn._spawn` —
|
|
||||||
keeping the `_spawn.py` path as the "core" module
|
|
||||||
avoids breaking external refs during Phase A.
|
|
||||||
|
|
||||||
Phase A commit breakdown (each green via `/run-tests`):
|
|
||||||
1. Drive `_testing/pytest.py` backend-tuple from
|
|
||||||
`get_args(SpawnMethodKey)` — prep.
|
|
||||||
2. Extract `trio_proc` → `spawn/_trio.py`; `_methods`
|
|
||||||
registry imports it.
|
|
||||||
3. Extract `mp_proc` → `spawn/_mp.py`; `_methods` imports
|
|
||||||
it.
|
|
||||||
4. Tidy `spawn/__init__.py` docstring for the new layout.
|
|
||||||
|
|
||||||
Workflow: Phase A runs in a new worktree via `/open-wkt
|
|
||||||
spawn_modularize` (branch `wkt/spawn_modularize`) and
|
|
||||||
lands as its own PR before Phase B/C returns to the
|
|
||||||
current `claude_bun_opus4.7_subint_spawner_backend`
|
|
||||||
branch.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
None this turn — design/planning only. The user updated
|
|
||||||
`ai/prompt-io/prompts/subints_spawner.md` themselves to
|
|
||||||
complete the truncated sentence on line 32 (harness-flag
|
|
||||||
reference).
|
|
||||||
|
|
||||||
Upcoming (Phase A, next turn):
|
|
||||||
- `tractor/spawn/_spawn.py` — trimmed to "core"
|
|
||||||
- `tractor/spawn/_trio.py` — new; holds `trio_proc()`
|
|
||||||
- `tractor/spawn/_mp.py` — new; holds `mp_proc()`
|
|
||||||
- `tractor/spawn/__init__.py` — docstring refresh
|
|
||||||
- `tractor/_testing/pytest.py` — backend tuple from
|
|
||||||
`get_args(SpawnMethodKey)`
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated. User completed the
|
|
||||||
truncated sentence on line 32 of
|
|
||||||
`ai/prompt-io/prompts/subints_spawner.md` themselves
|
|
||||||
*before* this prompt-io log was written; that edit was
|
|
||||||
the user's authoring, not an edit to AI output.
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-04-17T03:49:18Z
|
|
||||||
git_ref: 9703210
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
No code was produced in this turn — this is a design/planning
|
|
||||||
exchange for subint spawner support per issue #379. Captured
|
|
||||||
verbatim below are the two substantive AI responses, which
|
|
||||||
together fix the phased plan (A: modularize, B: subint, C:
|
|
||||||
harness) and the concrete Phase A file split before any
|
|
||||||
implementation begins.
|
|
||||||
|
|
||||||
## Turn 1 — initial triage + gating questions
|
|
||||||
|
|
||||||
> Read: issue #379, current `spawn/_spawn.py` (847 lines),
|
|
||||||
> and pyproject pins.
|
|
||||||
>
|
|
||||||
> ## Task summary
|
|
||||||
> Design/plan/implement subint backend per #379, plus
|
|
||||||
> modularize `spawn/_spawn.py` into per-backend submods, plus
|
|
||||||
> harness flag for test parametrization.
|
|
||||||
>
|
|
||||||
> ## Key finding
|
|
||||||
> `pyproject.toml:12` pins `requires-python = ">=3.12, <3.14"`
|
|
||||||
> — but `concurrent.interpreters` / PEP 734 **only lands in
|
|
||||||
> 3.14**. This is the load-bearing design question before
|
|
||||||
> coding.
|
|
||||||
>
|
|
||||||
> ## Proposed phasing (incremental commits)
|
|
||||||
> - **Phase A — modularize** `_spawn.py`: extract `trio_proc`
|
|
||||||
> → `spawn/_trio.py`, `mp_proc` → `spawn/_mp.py`, keep
|
|
||||||
> generic helpers (`hard_kill`/`soft_kill`/`new_proc`
|
|
||||||
> dispatcher/`SpawnMethodKey`) in a slim core. Tests green
|
|
||||||
> at each step.
|
|
||||||
> - **Phase B — `_subint` backend**: extend `SpawnMethodKey`
|
|
||||||
> with `'subint'`; reuse UDS transport by default; shm is
|
|
||||||
> escape-hatch follow-up.
|
|
||||||
> - **Phase C — harness**: add backend-parametrize flag, skip
|
|
||||||
> subint when Python < 3.14.
|
|
||||||
>
|
|
||||||
> ## Gating questions before I start
|
|
||||||
> 1. **Python pin**: bump `<3.15` and feature-gate subint
|
|
||||||
> tests (a), require 3.14 outright (b), or backport shim
|
|
||||||
> (c)? I'd recommend (a).
|
|
||||||
> 2. **PR strategy**: one branch, commits per phase (current
|
|
||||||
> `claude_bun_opus4.7_subint_spawner_backend`)? Or split
|
|
||||||
> Phase A as its own PR first?
|
|
||||||
> 3. **`fork()`-via-subint hack** from the issue's "Our own
|
|
||||||
> thoughts" section — v1 or follow-up? I'd defer to
|
|
||||||
> follow-up.
|
|
||||||
> 4. **Prompt file line 32 is truncated** — what's the
|
|
||||||
> existing harness flag name you had in mind ("using the
|
|
||||||
> existing ___")?
|
|
||||||
>
|
|
||||||
> Which of these are already decided vs. open?
|
|
||||||
|
|
||||||
## User answers (for the raw record)
|
|
||||||
|
|
||||||
1. bump `<3.15`, feature-gate — option (a).
|
|
||||||
2. split Phase A as its own PR first.
|
|
||||||
3. defer `fork()`-via-subint hack.
|
|
||||||
4. harness flag is `--spawn-backend` CLI → `start_method`
|
|
||||||
fixture, per `tractor._testing._pytest`. Prompt file
|
|
||||||
updated accordingly.
|
|
||||||
|
|
||||||
## Turn 2 — concrete Phase A plan
|
|
||||||
|
|
||||||
> Good — prompt file now ends cleanly; harness flag
|
|
||||||
> confirmed as `--spawn-backend` CLI → `start_method`
|
|
||||||
> fixture (via `_testing/pytest.py:193,355`). Also noted
|
|
||||||
> `pytest.py:345-349` hardcodes valid backends as a string
|
|
||||||
> tuple — `typing.get_args(SpawnMethodKey)` should drive
|
|
||||||
> that list.
|
|
||||||
>
|
|
||||||
> ## Phase A — concrete plan (separate PR)
|
|
||||||
>
|
|
||||||
> **File split** in `tractor/spawn/`:
|
|
||||||
> - `_spawn.py` stays as "core": keeps `SpawnMethodKey`,
|
|
||||||
> `_spawn_method`, `proc_waiter`, `try_set_start_method`,
|
|
||||||
> `exhaust_portal`, `cancel_on_completion`, `hard_kill`,
|
|
||||||
> `soft_kill`, `new_proc` dispatcher, `_methods`
|
|
||||||
> registry. Kept at this path so `_testing/pytest.py:228`
|
|
||||||
> and other external imports don't break.
|
|
||||||
> - `_trio.py` ← `trio_proc()` (lines 448–671, ~224 lines)
|
|
||||||
> - `_mp.py` ← `mp_proc()` (lines 672–842, ~171 lines)
|
|
||||||
> - `_methods` dict imports the two backend fns from the
|
|
||||||
> new modules.
|
|
||||||
>
|
|
||||||
> **Commits** (each green via `/run-tests`):
|
|
||||||
> 1. Drive `_testing/pytest.py` backend-tuple from
|
|
||||||
> `get_args(SpawnMethodKey)` — tiny prep.
|
|
||||||
> 2. Extract `trio_proc` → `spawn/_trio.py`; `_methods`
|
|
||||||
> imports it.
|
|
||||||
> 3. Extract `mp_proc` → `spawn/_mp.py`; `_methods`
|
|
||||||
> imports it.
|
|
||||||
> 4. Tidy `spawn/__init__.py` docstring to describe new
|
|
||||||
> layout.
|
|
||||||
>
|
|
||||||
> No pyproject pin bump in Phase A (that's Phase B).
|
|
||||||
>
|
|
||||||
> **Workflow**: I'd run this in a new worktree via
|
|
||||||
> `/open-wkt spawn_modularize` (branch
|
|
||||||
> `wkt/spawn_modularize`), then open a PR before returning
|
|
||||||
> to the current `claude_bun_opus4.7_subint_spawner_backend`
|
|
||||||
> branch for Phase B/C.
|
|
||||||
>
|
|
||||||
> Green-light to `/open-wkt` and start Phase A?
|
|
||||||
|
|
||||||
## User final response
|
|
||||||
|
|
||||||
> all this first then yes, green light.
|
|
||||||
|
|
||||||
— log this `/prompt-io` entry first, then proceed with
|
|
||||||
Phase A in a worktree.
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
session: subints-spawner-phase-a-impl
|
|
||||||
timestamp: 2026-04-17T03:58:05Z
|
|
||||||
git_ref: 61a73ba
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260417T035800Z_61a73ba_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Follow-up from the design session logged in
|
|
||||||
`20260417T034918Z_9703210_prompt_io.md`: the user
|
|
||||||
greenlit Phase A after `/prompt-io` tracking, so
|
|
||||||
execute the Phase A "modularize `spawn._spawn`" plan
|
|
||||||
end-to-end in a new worktree — four logical steps
|
|
||||||
(pytest harness prep, `trio_proc` extraction,
|
|
||||||
`mp_proc` extraction, docstring refresh), each green
|
|
||||||
via `/run-tests`.
|
|
||||||
|
|
||||||
User directives during execution:
|
|
||||||
- **Option 3** for the test cadence ("continue A.2–A.4
|
|
||||||
first, then run the full suite once at the end of
|
|
||||||
Phase A").
|
|
||||||
- **One commit** for the whole phase ("can't we just
|
|
||||||
commit the whole patch in one commit?") instead of
|
|
||||||
the 3/4-commit split I initially proposed.
|
|
||||||
- **Don't pre-draft** commit messages — wait for the
|
|
||||||
user to invoke `/commit-msg` (captured as feedback
|
|
||||||
memory `feedback_no_auto_draft_commit_msgs.md`).
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Produced the cohesive Phase A modularization patch,
|
|
||||||
landed as commit `61a73bae` (subject: `Mv
|
|
||||||
trio_proc`/`mp_proc` to per-backend submods`). Five
|
|
||||||
files changed, +565 / -418 lines.
|
|
||||||
|
|
||||||
Key pieces of the patch (generated by claude,
|
|
||||||
reviewed by the human before commit):
|
|
||||||
- `tractor/spawn/_trio.py` — **new**; receives
|
|
||||||
`trio_proc()` verbatim from `_spawn.py`; imports
|
|
||||||
cross-backend helpers back from `._spawn`.
|
|
||||||
- `tractor/spawn/_mp.py` — **new**; receives
|
|
||||||
`mp_proc()` verbatim; uses `from . import _spawn`
|
|
||||||
for late-binding access to the mutable `_ctx` /
|
|
||||||
`_spawn_method` globals (design decision made
|
|
||||||
during impl, not the original plan).
|
|
||||||
- `tractor/spawn/_spawn.py` — shrunk 847 → 448 LOC;
|
|
||||||
import pruning; bottom-of-module late imports for
|
|
||||||
`trio_proc` / `mp_proc` with a one-line comment
|
|
||||||
explaining the circular-dep reason.
|
|
||||||
- `tractor/spawn/__init__.py` — docstring refresh
|
|
||||||
describing the new layout.
|
|
||||||
- `tractor/_testing/pytest.py` — the valid-backend
|
|
||||||
set now comes from `typing.get_args(SpawnMethodKey)`
|
|
||||||
so future additions (`'subint'`) don't need harness
|
|
||||||
edits.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
See `git diff 61a73ba~1..61a73ba --stat`:
|
|
||||||
|
|
||||||
```
|
|
||||||
tractor/_testing/pytest.py | 12 +-
|
|
||||||
tractor/spawn/__init__.py | 31 +++-
|
|
||||||
tractor/spawn/_mp.py | 235 ++++++++++++++++++++++++
|
|
||||||
tractor/spawn/_spawn.py | 413 +-------------------------------
|
|
||||||
tractor/spawn/_trio.py | 292 ++++++++++++++++++++++++++++
|
|
||||||
5 files changed, 565 insertions(+), 418 deletions(-)
|
|
||||||
```
|
|
||||||
|
|
||||||
Validation:
|
|
||||||
- import probe + `_methods` wiring check — OK
|
|
||||||
- spawn-relevant test subset — 37 passed, 1 skipped
|
|
||||||
- full suite — 350 passed, 14 skipped, 7 xfailed, 1
|
|
||||||
xpassed
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated by claude (no diff
|
|
||||||
between `.claude/git_commit_msg_LATEST.md` and the
|
|
||||||
committed body, as far as the assistant could
|
|
||||||
observe).
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-04-17T03:58:05Z
|
|
||||||
git_ref: 61a73ba
|
|
||||||
diff_cmd: git diff 61a73ba~1..61a73ba
|
|
||||||
---
|
|
||||||
|
|
||||||
Code generated in this turn was committed verbatim as
|
|
||||||
`61a73bae` ("Mv `trio_proc`/`mp_proc` to per-backend
|
|
||||||
submods"). Per diff-ref mode, per-file code is captured
|
|
||||||
via the pointers below, each followed by a prose
|
|
||||||
summary of what the AI generated. Non-code output
|
|
||||||
(sanity-check results, design rationale) is included
|
|
||||||
verbatim.
|
|
||||||
|
|
||||||
## Per-file generated content
|
|
||||||
|
|
||||||
### `tractor/spawn/_trio.py` (new, 292 lines)
|
|
||||||
|
|
||||||
> `git diff 61a73ba~1..61a73ba -- tractor/spawn/_trio.py`
|
|
||||||
|
|
||||||
Pure lift-and-shift of `trio_proc()` out of
|
|
||||||
`tractor/spawn/_spawn.py` (previously lines 448–670).
|
|
||||||
Added AGPL header + module docstring describing the
|
|
||||||
backend; imports include local `from ._spawn import
|
|
||||||
cancel_on_completion, hard_kill, soft_kill` which
|
|
||||||
creates the bottom-of-module late-import pattern in
|
|
||||||
the core file to avoid a cycle. All call sites,
|
|
||||||
log-format strings, and body logic are byte-identical
|
|
||||||
to the originals — no semantic change.
|
|
||||||
|
|
||||||
### `tractor/spawn/_mp.py` (new, 235 lines)
|
|
||||||
|
|
||||||
> `git diff 61a73ba~1..61a73ba -- tractor/spawn/_mp.py`
|
|
||||||
|
|
||||||
Pure lift-and-shift of `mp_proc()` out of
|
|
||||||
`tractor/spawn/_spawn.py` (previously lines 672–842).
|
|
||||||
Same AGPL header convention. Key difference from
|
|
||||||
`_trio.py`: uses `from . import _spawn` (module
|
|
||||||
import, not from-import) for `_ctx` and
|
|
||||||
`_spawn_method` references — these are mutated at
|
|
||||||
runtime by `try_set_start_method()`, so late binding
|
|
||||||
via `_spawn._ctx` / `_spawn._spawn_method` is required
|
|
||||||
for correctness. Also imports `cancel_on_completion`,
|
|
||||||
`soft_kill`, `proc_waiter` from `._spawn`.
|
|
||||||
|
|
||||||
### `tractor/spawn/_spawn.py` (modified, 847 → 448 LOC)
|
|
||||||
|
|
||||||
> `git diff 61a73ba~1..61a73ba -- tractor/spawn/_spawn.py`
|
|
||||||
|
|
||||||
- removed `trio_proc()` body (moved to `_trio.py`)
|
|
||||||
- removed `mp_proc()` body (moved to `_mp.py`)
|
|
||||||
- pruned imports now unused in core: `sys`,
|
|
||||||
`is_root_process`, `current_actor`,
|
|
||||||
`is_main_process`, `_mp_main`, `ActorFailure`,
|
|
||||||
`pretty_struct`, `_pformat`
|
|
||||||
- added bottom-of-file late imports
|
|
||||||
`from ._trio import trio_proc` and
|
|
||||||
`from ._mp import mp_proc` with a one-line
|
|
||||||
comment explaining why (circular dep)
|
|
||||||
- `_methods` dict unchanged structurally; still binds
|
|
||||||
`'trio' → trio_proc`, `'mp_spawn' → mp_proc`,
|
|
||||||
`'mp_forkserver' → mp_proc`
|
|
||||||
|
|
||||||
### `tractor/spawn/__init__.py` (modified)
|
|
||||||
|
|
||||||
> `git diff 61a73ba~1..61a73ba -- tractor/spawn/__init__.py`
|
|
||||||
|
|
||||||
Docstring rewrite only — no code. New text describes
|
|
||||||
the per-backend `Layout` with `- ._spawn` (core),
|
|
||||||
`- ._trio` (default), `- ._mp` (two mp variants), and
|
|
||||||
reiterates the "no eager import" NOTE.
|
|
||||||
|
|
||||||
### `tractor/_testing/pytest.py` (modified)
|
|
||||||
|
|
||||||
> `git diff 61a73ba~1..61a73ba -- tractor/_testing/pytest.py`
|
|
||||||
|
|
||||||
Two-line logical change (plus a comment):
|
|
||||||
- `from typing import` grows `get_args`
|
|
||||||
- `pytest_generate_tests()` replaces the hardcoded
|
|
||||||
`('mp_spawn','mp_forkserver','trio')` tuple with
|
|
||||||
`get_args(SpawnMethodKey)` via lazy import
|
|
||||||
`from tractor.spawn._spawn import SpawnMethodKey`
|
|
||||||
inside the function body (matching the project's
|
|
||||||
existing lazy-import convention for the spawn
|
|
||||||
module)
|
|
||||||
|
|
||||||
## Non-code output (verbatim)
|
|
||||||
|
|
||||||
### Design rationale for the file split
|
|
||||||
|
|
||||||
From my Phase A plan turn (already captured in
|
|
||||||
`20260417T034918Z_9703210_prompt_io.md`):
|
|
||||||
- `_spawn.py` stays as the "core" rather than renamed
|
|
||||||
to `_core.py` so external imports
|
|
||||||
(`_testing/pytest.py:228` imports
|
|
||||||
`try_set_start_method` from `tractor.spawn._spawn`)
|
|
||||||
keep working without churn.
|
|
||||||
- Per-backend extraction chosen over alternatives
|
|
||||||
(e.g. splitting generic helpers further) because
|
|
||||||
the immediate motivation is hosting a 3rd
|
|
||||||
`_subint.py` sibling cleanly in Phase B.
|
|
||||||
|
|
||||||
### Sanity-check output (verbatim terminal excerpts)
|
|
||||||
|
|
||||||
Post-extraction import probe:
|
|
||||||
```
|
|
||||||
extraction OK
|
|
||||||
_methods: {'trio': 'tractor.spawn._trio.trio_proc',
|
|
||||||
'mp_spawn': 'tractor.spawn._mp.mp_proc',
|
|
||||||
'mp_forkserver': 'tractor.spawn._mp.mp_proc'}
|
|
||||||
```
|
|
||||||
|
|
||||||
Spawn-relevant test subset (`tests/test_local.py
|
|
||||||
test_rpc.py test_spawning.py test_multi_program.py
|
|
||||||
test_discovery.py`):
|
|
||||||
```
|
|
||||||
37 passed, 1 skipped, 14 warnings in 55.37s
|
|
||||||
```
|
|
||||||
|
|
||||||
Full suite:
|
|
||||||
```
|
|
||||||
350 passed, 14 skipped, 7 xfailed, 1 xpassed,
|
|
||||||
151 warnings in 437.73s (0:07:17)
|
|
||||||
```
|
|
||||||
|
|
||||||
No regressions vs. `main`. One transient `-x`
|
|
||||||
early-stop `ERROR` on
|
|
||||||
`test_close_channel_explicit_remote_registrar[trio-True]`
|
|
||||||
was flaky (passed solo, passed without `-x`), not
|
|
||||||
caused by this refactor.
|
|
||||||
|
|
||||||
### Commit message
|
|
||||||
|
|
||||||
Also AI-drafted (via `/commit-msg`) — the 40-line
|
|
||||||
message on commit `61a73bae` itself. Not reproduced
|
|
||||||
here; see `git log -1 61a73bae`.
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
session: trio-0.33-subproc-supervisor-retroactive
|
|
||||||
timestamp: 2026-06-01T23:14:29Z
|
|
||||||
git_ref: 0e3e008b
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260601T231429Z_0e3e008b_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
**RETROACTIVE LOG** — original session prompts not
|
|
||||||
preserved; reconstructed from the staged work product.
|
|
||||||
|
|
||||||
The work designs a `trio.Nursery.start()`-style wrapper
|
|
||||||
around `trio.run_process()` for SC-friendly subprocess
|
|
||||||
supervision. From the resulting code shape, the
|
|
||||||
prompting intent was:
|
|
||||||
|
|
||||||
1. Surface rc!=0 `CalledProcessError` DETERMINISTICALLY,
|
|
||||||
without the nursery-eg-wrapping that complicates
|
|
||||||
`collapse_eg()` usage and races the relay reader on
|
|
||||||
trio's `check=True`-driven cancel cascade.
|
|
||||||
2. ALWAYS isolate the parent controlling-tty so a
|
|
||||||
spawned child can't emit terminal control-seqs onto
|
|
||||||
the launching tty (clobbering scrollback). Default
|
|
||||||
`stdin=DEVNULL`; default `stdout=DEVNULL` unless
|
|
||||||
explicitly relayed/overridden; distinguish "caller
|
|
||||||
passed nothing" from "caller passed `None` for
|
|
||||||
inherit".
|
|
||||||
3. Optional live per-line relay of child std-streams to
|
|
||||||
the `tractor` log — STREAMED (not
|
|
||||||
buffered-until-exit) so long-lived daemon output is
|
|
||||||
visible during the run. Pick a custom log level that
|
|
||||||
shows at usual `info`/`devx` console levels but is
|
|
||||||
separately filterable.
|
|
||||||
4. Concurrent pipe-drain reader MANDATORY when piping
|
|
||||||
without `capture_*` — without it the child blocks on
|
|
||||||
`write()` once the OS pipe buffer fills (~64KiB),
|
|
||||||
causing deadlocks on output bursts.
|
|
||||||
5. Non-blocking `tn.start()` semantics: hand the live
|
|
||||||
`trio.Process` to the parent immediately;
|
|
||||||
supervise/relay run to completion in the supervisor
|
|
||||||
coro.
|
|
||||||
6. Hermetic `trio`-only unit tests (no actor-runtime)
|
|
||||||
covering each of: per-line relay, tty isolation,
|
|
||||||
no-deadlock on >64KiB unnewlined output, CPE
|
|
||||||
rebuild w/ stderr relay, CPE rebuild on the silent
|
|
||||||
drain+capture path.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Adds `tractor/trionics/_subproc.py` (296 LOC) +
|
|
||||||
`tests/trionics/test_subproc.py` (230 LOC) + a
|
|
||||||
re-export in `tractor/trionics/__init__.py`.
|
|
||||||
|
|
||||||
**`supervise_run_process()`** (public, re-exported)
|
|
||||||
- `check=False` is forced to `trio.run_process`; the
|
|
||||||
rc-check runs in the supervisor coro AFTER `own_tn`
|
|
||||||
unwinds (both the child AND the relay readers have
|
|
||||||
hit EOF + fully drained). A BARE
|
|
||||||
`subprocess.CalledProcessError` is rebuilt + raised
|
|
||||||
from there, with `.stderr` bytes passed in the
|
|
||||||
constructor AND attached as an `add_note()`'d
|
|
||||||
`|_.stderr:` block for legible teardown logs.
|
|
||||||
- `stdin=DEVNULL` always. `stdout` default chosen via a
|
|
||||||
`_UNSET` sentinel: `relay_stdout=True` → PIPE,
|
|
||||||
explicit `stdout=...` → as given, else `DEVNULL`.
|
|
||||||
`stderr` defaults to PIPE whenever we relay OR need
|
|
||||||
the CPE note (when `check=True`), else `DEVNULL`.
|
|
||||||
- `relay_level='io'` (custom level 21; sorts just
|
|
||||||
above stdlib `INFO`=20 so it shows at usual
|
|
||||||
`info`/`devx` levels and stays separately
|
|
||||||
filterable). `runtime`=15 would silently filter at
|
|
||||||
default levels, so it's rejected as a default.
|
|
||||||
- `task_status.started(trio_proc)` delivers the live
|
|
||||||
process immediately. The internal `own_tn`
|
|
||||||
supervises `trio.run_process` + any relay readers to
|
|
||||||
completion.
|
|
||||||
- `**run_process_kwargs` forward verbatim;
|
|
||||||
`stdin/stdout/stderr/check` are MANAGED keys
|
|
||||||
(override on conflict).
|
|
||||||
- Crash-handling deliberately NOT baked in — compose
|
|
||||||
`maybe_open_crash_handler()` on top at the call-site.
|
|
||||||
|
|
||||||
**`_relay_stream_lines()`** (internal helper)
|
|
||||||
- Three modes (combinable): `emit`-only (live per-line
|
|
||||||
relay), `accum`-only (silent drain+capture for a CPE
|
|
||||||
note), or both (live relay AND capture).
|
|
||||||
- Per-line split handles cross-chunk residuals via a
|
|
||||||
rolling `residual` bytes buffer; flushes any trailing
|
|
||||||
un-newline-term'd line at EOF.
|
|
||||||
- `async with stream:` ensures aclose at EOF/cancel
|
|
||||||
(mirrors trio's internal `_subprocess` drain idiom).
|
|
||||||
|
|
||||||
**`_add_stderr_note()`** (internal helper)
|
|
||||||
- `add_note()`s a `textwrap.indent(...)`'d
|
|
||||||
`|_.stderr:` block onto a `CalledProcessError` for
|
|
||||||
teardown logs.
|
|
||||||
|
|
||||||
**Tests** (5 hermetic, trio-only) — `_capture_relay`
|
|
||||||
fixture monkeypatches `_subproc.log.<level>` to a list:
|
|
||||||
- `test_stdout_relayed_per_line`: per-line stdout
|
|
||||||
relay carries each `line=N` to the records.
|
|
||||||
- `test_parent_tty_isolated`: `readlink /proc/self/fd/0`
|
|
||||||
and `fd/1` from the child show `pipe:` (fd1) +
|
|
||||||
`/dev/null` (fd0); NO `/dev/pts/*`.
|
|
||||||
- `test_no_deadlock_on_big_unnewlined_output`: 200KiB
|
|
||||||
of `x` with no newlines completes inside
|
|
||||||
`fail_after(2)` — exercises the concurrent drain.
|
|
||||||
- `test_stderr_relay_and_cpe_rebuild`: rc=3 with
|
|
||||||
`relay_stderr=True` raises bare CPE
|
|
||||||
(via `collapse_eg()`) with `b'boom' in cpe.stderr`,
|
|
||||||
the note attached, AND per-line live relay.
|
|
||||||
- `test_nonrelay_cpe_note`: rc=7 with no relay still
|
|
||||||
produces CPE with `.stderr` + note via the silent
|
|
||||||
drain+capture path.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_subproc.py` — NEW. Public
|
|
||||||
`supervise_run_process()` + helpers
|
|
||||||
`_relay_stream_lines()` / `_add_stderr_note()` + the
|
|
||||||
`_UNSET` sentinel.
|
|
||||||
- `tests/trionics/test_subproc.py` — NEW. 5 hermetic
|
|
||||||
trio-only tests + `_capture_relay` monkeypatch
|
|
||||||
fixture.
|
|
||||||
- `tractor/trionics/__init__.py` — re-export
|
|
||||||
`supervise_run_process`.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
**RETROACTIVE**: this log is being written from the
|
|
||||||
staged diff, not from a live session. The code as
|
|
||||||
staged is the canonical artifact; any human edits the
|
|
||||||
user made during the originating design session are
|
|
||||||
already integrated and cannot be separated post-hoc.
|
|
||||||
The `.raw.md` sibling is a diff-pointer placeholder,
|
|
||||||
NOT a pre-edit transcript.
|
|
||||||
|
|
||||||
Future prompt-io entries for in-flight work should be
|
|
||||||
written DURING the design session per the skill
|
|
||||||
contract so the pre-edit `.raw.md` captures the
|
|
||||||
unedited model output for genuine provenance.
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-7[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-06-01T23:14:29Z
|
|
||||||
git_ref: 0e3e008b
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
# RETROACTIVE — original model output not preserved
|
|
||||||
|
|
||||||
This `.raw.md` would normally contain the verbatim
|
|
||||||
pre-human-edit response from the design session that
|
|
||||||
produced the staged `_subproc.py` module + tests. That
|
|
||||||
session's transcript is not available, so this file
|
|
||||||
serves as a diff-pointer placeholder + transparency
|
|
||||||
note.
|
|
||||||
|
|
||||||
## Authoritative artifact
|
|
||||||
|
|
||||||
The committed code IS the artifact of record. Once the
|
|
||||||
companion commit lands, the unified diff is:
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_subproc.py`
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/trionics/test_subproc.py`
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/__init__.py`
|
|
||||||
|
|
||||||
Before committing, substitute `--cached` for the
|
|
||||||
pre-commit form.
|
|
||||||
|
|
||||||
## What is NOT here
|
|
||||||
|
|
||||||
Because this is retroactive:
|
|
||||||
- No verbatim chain-of-thought / discussion prose from
|
|
||||||
the design session.
|
|
||||||
- No rejected alternatives the model considered before
|
|
||||||
arriving at the final shape (e.g. whether the
|
|
||||||
rc-check should live inside `own_tn` vs after it; the
|
|
||||||
`_UNSET` sentinel vs a `None`-means-DEVNULL
|
|
||||||
convention; `io` vs `info` as the default relay
|
|
||||||
level).
|
|
||||||
- No pre-edit code blocks as the model first emitted
|
|
||||||
them, separable from any user cleanup applied before
|
|
||||||
the diff was staged.
|
|
||||||
|
|
||||||
## Inferred design choices visible in the final code
|
|
||||||
|
|
||||||
(Documented here because they're the kind of decision
|
|
||||||
detail an unedited raw transcript would have captured.)
|
|
||||||
|
|
||||||
1. **Post-drain rc-check in the supervisor coro body,
|
|
||||||
AFTER `own_tn.__aexit__`.** Placing the
|
|
||||||
`CalledProcessError` raise here (not inside
|
|
||||||
`own_tn`) means the EG-unwrap happens at the OUTER
|
|
||||||
`tn.start()` boundary — callers do `collapse_eg()`
|
|
||||||
if they want bare. Doing the raise INSIDE `own_tn`
|
|
||||||
would cancel the still-draining relay reader
|
|
||||||
mid-flight and lose stderr lines.
|
|
||||||
|
|
||||||
2. **`_UNSET` sentinel for `stdout`.** A plain default
|
|
||||||
of `None` couldn't distinguish "use the safe
|
|
||||||
`DEVNULL` default" from "caller explicitly passed
|
|
||||||
`None` (inherit, presumably knowingly)". The
|
|
||||||
sentinel keeps the SAFE default while letting power
|
|
||||||
users opt into inherit.
|
|
||||||
|
|
||||||
3. **`relay_level='io'` (custom level 21).** Chosen to
|
|
||||||
sort just above stdlib `INFO`=20 so a default
|
|
||||||
`--ll info` shows the relay, but it remains a
|
|
||||||
distinct level so users can filter
|
|
||||||
`tractor.trionics:io` separately. Picking
|
|
||||||
`runtime`=15 would have made the relay invisible at
|
|
||||||
default verbosity (a footgun for daemon supervisors
|
|
||||||
whose whole point is "I want to see this output").
|
|
||||||
|
|
||||||
4. **Reader is MANDATORY, not opt-in cosmetic.** With
|
|
||||||
`stdout=PIPE` / `stderr=PIPE` we OWN the drain
|
|
||||||
responsibility — there's no `trio.capture_*` running
|
|
||||||
under the hood here. The ~64KiB OS pipe buffer
|
|
||||||
means a child writing more than that without us
|
|
||||||
reading hangs at `write()` — a deadlock that won't
|
|
||||||
show up in small-output tests, which is why the
|
|
||||||
200KiB-no-newline test is in the suite.
|
|
||||||
|
|
||||||
5. **`task_status.started(trio_proc)` BEFORE the
|
|
||||||
`own_tn` exits.** Without this, `tn.start()` would
|
|
||||||
block until the child exits — losing the "start a
|
|
||||||
long-lived daemon and continue with parent work"
|
|
||||||
use case. With it, the parent gets the live process
|
|
||||||
handle immediately and the supervise+relay tasks
|
|
||||||
run in the supervisor coro until the child exits.
|
|
||||||
|
|
||||||
6. **`__notes__` via `add_note()` for the CPE
|
|
||||||
`.stderr`.** The `.stderr` attribute is what
|
|
||||||
`subprocess` callers expect; the `add_note()` is
|
|
||||||
what trio's exception-rendering shows. Both wired so
|
|
||||||
programmatic AND human consumers see the stderr at
|
|
||||||
teardown.
|
|
||||||
|
|
||||||
## Honesty statement
|
|
||||||
|
|
||||||
This file's content is RECONSTRUCTED from the staged
|
|
||||||
code, not extracted from a verbatim model transcript.
|
|
||||||
The prompt-io skill's intent is for the `.raw.md` to
|
|
||||||
be a pre-edit fossil; that's not possible here. Future
|
|
||||||
work should write the prompt-io entry DURING the
|
|
||||||
design session.
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
session: 0780a862-e19a-4f0a-86cd-c8afc0997757
|
|
||||||
timestamp: 2026-06-11T17:51:52Z
|
|
||||||
git_ref: 8526985c
|
|
||||||
scope: docs+code+config
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260611T175152Z_8526985c_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
> we need big boi docs; ours are way out of date and generally
|
|
||||||
> terrible. i'd like to use a sphinx theme in the vein and/or
|
|
||||||
> one-of/nearly-the-same-as used in one or all of the `msgspec`,
|
|
||||||
> `numpy`, `ray`, `xonsh`, `polars` projects.
|
|
||||||
>
|
|
||||||
> see the following oustanding but now very old issues to guide
|
|
||||||
> you: #175, #126, #123, #157
|
|
||||||
>
|
|
||||||
> we should try to target a 3 columned sphinx theme with,
|
|
||||||
> - document index-nav on left,
|
|
||||||
> - body content in middle,
|
|
||||||
> - diagrams (ideally in d2lang if possible) on RHS
|
|
||||||
>
|
|
||||||
> optimize for (if possible),
|
|
||||||
> - reusing examples code without duplication in docs,
|
|
||||||
> - generating new examples from todos throughout code base,
|
|
||||||
> - distilling the essence of SC across processes in the simplest
|
|
||||||
> and most friendly way possible.
|
|
||||||
>
|
|
||||||
> do this work in a /open-wkt and do you best without my guidance
|
|
||||||
> for the first major pass - we will refine asap once you are
|
|
||||||
> complete.
|
|
||||||
|
|
||||||
Session settings: `/effort max` + ultracode (multi-agent workflow
|
|
||||||
orchestration). Fully autonomous first pass; two orchestrated
|
|
||||||
agent fleets were used (6-agent recon survey, then 10-agent
|
|
||||||
content fan-out: 9 section writers + 1 examples smith), with the
|
|
||||||
orchestrator authoring the landing page, SC essay, sphinx
|
|
||||||
scaffold, d2 pipeline + diagram sources, CI workflow and all
|
|
||||||
integration/fix passes directly.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Complete sphinx docs revamp on branch `wkt/big_boi_docs`:
|
|
||||||
`pydata-sphinx-theme` 0.18 (3-column: left nav / content /
|
|
||||||
page-toc, with d2 diagrams + asides floated into the RHS margin
|
|
||||||
via custom CSS), sphinx 9.1, a local `.. d2::` directive
|
|
||||||
rendering `docs/diagrams/*.d2` sources with committed-SVG
|
|
||||||
fallback, a vendored `.. margin::` directive, ~25 new doc pages
|
|
||||||
(landing, start/, explain/, 12 guides, 10 api-ref pages,
|
|
||||||
project/), 5 new auto-tested examples + 3 modernized + 1
|
|
||||||
renamed, and a gh-pages deploy workflow (issue #123). All
|
|
||||||
example code is `literalinclude`d from `examples/` (zero
|
|
||||||
duplication, CI-verified). Build: green, 24 warnings all
|
|
||||||
pre-existing-docstring/NEWS sourced.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
See the branch diff (uncommitted at entry-write time):
|
|
||||||
|
|
||||||
> `git diff test_cpu_throttling..wkt/big_boi_docs`
|
|
||||||
> `git -C <wkt> status --short` (pre-commit working tree)
|
|
||||||
|
|
||||||
- `docs/conf.py` — full rewrite for pydata theme + ext stack
|
|
||||||
- `docs/_ext/d2diagrams.py` — new `.. d2::` sphinx directive
|
|
||||||
- `docs/_ext/marginalia.py` — new `.. margin::` directive
|
|
||||||
- `docs/_static/css/custom.css` — b&w skin + RHS margin floats
|
|
||||||
- `docs/diagrams/*.d2` (7) — diagram sources (sketch/grayscale)
|
|
||||||
- `docs/_diagrams/*.svg` (7) — committed rendered fallbacks
|
|
||||||
- `docs/index.rst` — new landing (replaces dead-API doc)
|
|
||||||
- `docs/start/*.rst` (3), `docs/explain/*.rst` (3),
|
|
||||||
`docs/guide/*.rst` (13), `docs/api/*.rst` (10),
|
|
||||||
`docs/project/*.rst` (3) — new content tree
|
|
||||||
- `docs/dev_tips.rst` — removed (ported to project/dev-tips)
|
|
||||||
- `examples/{typed_payloads,nested_actor_tree,
|
|
||||||
service_daemon_discovery,uds_transport_actor_tree,
|
|
||||||
streaming_broadcast_fanout}.py` — new, smoke-tested
|
|
||||||
- `examples/{a_trynamic_first_scene,
|
|
||||||
actor_spawning_and_causality,parallelism/single_func}.py` —
|
|
||||||
`.result()` -> `.wait_for_result()` modernization
|
|
||||||
- `examples/parallelism/concurrent_futures_primes.py` — renamed
|
|
||||||
from leading-underscore + trio-runner shim added
|
|
||||||
- `pyproject.toml` — `docs` dependency-group filled in
|
|
||||||
- `uv.lock` — relock for docs group
|
|
||||||
- `.github/workflows/docs.yml` — build + gh-pages deploy
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — entry written pre-commit; the author reviews, stages
|
|
||||||
and commits manually (per repo workflow policy).
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-06-11T17:51:52Z
|
|
||||||
git_ref: 8526985c
|
|
||||||
diff_cmd: git diff test_cpu_throttling..wkt/big_boi_docs
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output pointers (diff-ref mode)
|
|
||||||
|
|
||||||
All generated content is code/config/docs committed alongside
|
|
||||||
this entry on branch `wkt/big_boi_docs`; per the prompt-io
|
|
||||||
diff-ref decision rule each file's verbatim content is the diff
|
|
||||||
itself rather than a copy here:
|
|
||||||
|
|
||||||
> `git diff test_cpu_throttling..wkt/big_boi_docs -- docs/`
|
|
||||||
> `git diff test_cpu_throttling..wkt/big_boi_docs -- examples/`
|
|
||||||
> `git diff test_cpu_throttling..wkt/big_boi_docs -- pyproject.toml uv.lock .github/`
|
|
||||||
|
|
||||||
## Generation notes (non-code output summary)
|
|
||||||
|
|
||||||
- Theme research (web, agent-verified 2026-06-11): msgspec=furo,
|
|
||||||
xonsh=furo, numpy/ray/polars=pydata-sphinx-theme (ray migrated
|
|
||||||
off sphinx-book-theme); sphinx-book-theme 1.2.0 hard-pins
|
|
||||||
pydata 0.16.1 (stale) -> chose pydata 0.18 + sphinx 9.1.
|
|
||||||
- d2 ecosystem: no production-grade pypi extension exists
|
|
||||||
(sphinxcontrib-d2lang 0.0.5 ignores returncodes, uuid4 output
|
|
||||||
names; sphinx-d2 is an empty stub) -> wrote local
|
|
||||||
`docs/_ext/d2diagrams.py` (~230 LOC) with D2_BIN env
|
|
||||||
discovery, mtime caching, committed-SVG fallback and
|
|
||||||
literal-block last resort.
|
|
||||||
- Diagrams authored in d2 (theme-id 1 "Neutral Grey" + sketch
|
|
||||||
mode + ELK layout, validated by render + headless-firefox
|
|
||||||
screenshot loop): actor_tree, context_handshake (real
|
|
||||||
msg-spec names Start/StartAck/Started/Yield/Stop/Return),
|
|
||||||
streaming_pipeline, runtime_stack, debug_lock,
|
|
||||||
error_propagation, infected_aio.
|
|
||||||
- API truth enforced from a 6-agent recon pass over the
|
|
||||||
reorganized package tree (runtime/, discovery/, spawn/, ipc/,
|
|
||||||
msg/, devx/, trionics/): docs teach `.wait_for_result()`,
|
|
||||||
registrar (not arbiter) naming, `@tractor.context` +
|
|
||||||
`open_context()` as the core model, `run_in_actor()` as
|
|
||||||
convenience only.
|
|
||||||
- All ~30 literalincluded example scripts verified present; 9
|
|
||||||
touched/new example files smoke-run green (exit 0, <16s).
|
|
||||||
- Final build: `sphinx-build -b html` succeeded; 24 residual
|
|
||||||
warnings, every one sourced from pre-existing library
|
|
||||||
docstring rst-isms or legacy NEWS.rst content (left untouched
|
|
||||||
by design; flagged for a follow-up docstring lint pass).
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
session: 638d5a98-abd2-4217-a447-19f2330eb334
|
|
||||||
timestamp: 2026-06-25T23:25:06Z
|
|
||||||
git_ref: 0a3c48fc
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260625T232506Z_0a3c48fc_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Same `/code-review` follow-up session (PR #460). Finding #10:
|
|
||||||
the new `api/` autodoc reference pages surfaced 22 docutils
|
|
||||||
warnings sourced from informal reST in public docstrings. The
|
|
||||||
user opted to fix it on this branch,
|
|
||||||
|
|
||||||
> well we're on a clean main so why not just do these now too?
|
|
||||||
|
|
||||||
Goal: drive the docs build warning-free (24 -> 0) without any
|
|
||||||
code/behaviour change — docstring content only.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Surgical docstring-RST lint across 9 library modules + the one
|
|
||||||
offending `NEWS.rst` changelog entry,
|
|
||||||
|
|
||||||
- bullet lists given a blank line + base-column indent
|
|
||||||
(`Context`, `Context.cancel_called`/`.cancelled_caught`/
|
|
||||||
`.outcome`, `ActorNursery.cancel_called`, `query_actor`,
|
|
||||||
`open_crash_handler`, the `#318` `NEWS.rst` entry),
|
|
||||||
- the under-short `Behaviour:` underline in `Context.cancel`
|
|
||||||
demoted to a `**bold**` label,
|
|
||||||
- unbalanced/pluralized-role backticks fixed — closed the
|
|
||||||
`wait_for_actor` summary backtick; applied the `` `role`\ s ``
|
|
||||||
escaped-plural idiom in `gather_contexts`, `mk_pdb`,
|
|
||||||
`MsgCodec`, msg `Error`, `open_context_from_portal`,
|
|
||||||
- the `|_` method-tree in `ContextCancelled.canceller` made a
|
|
||||||
literal block (bare `|` was parsed as a substitution ref).
|
|
||||||
|
|
||||||
Verified: build `24 -> 0` warnings; `import tractor` clean under
|
|
||||||
`-W error::SyntaxWarning`; the `\ s` idiom renders as e.g.
|
|
||||||
"acms" (no backslash/space leak in HTML); `ruff` clean; diff
|
|
||||||
confirmed docstring-content-only.
|
|
||||||
|
|
||||||
Initial generation was delegated to a constrained subagent
|
|
||||||
(docstring-only edits + a rebuild-to-zero verification gate);
|
|
||||||
the orchestrator independently re-built, reviewed the full
|
|
||||||
diff, and added the `NEWS.rst` fix to reach zero.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/_context.py`, `tractor/_exceptions.py`,
|
|
||||||
`tractor/devx/debug/_post_mortem.py`,
|
|
||||||
`tractor/devx/debug/_repl.py`, `tractor/discovery/_api.py`,
|
|
||||||
`tractor/msg/_codec.py`, `tractor/msg/types.py`,
|
|
||||||
`tractor/runtime/_supervise.py`,
|
|
||||||
`tractor/trionics/_mngrs.py` — docstring reST fixes,
|
|
||||||
- `NEWS.rst` — blank line before a bullet list in the `#318`
|
|
||||||
entry.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated (`0a3c48fc`).
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-06-25T23:25:06Z
|
|
||||||
git_ref: 0a3c48fc
|
|
||||||
diff_cmd: git diff 0a3c48fc~1..0a3c48fc
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output pointers (diff-ref mode)
|
|
||||||
|
|
||||||
The generated patch is committed; the verbatim content is the
|
|
||||||
diff:
|
|
||||||
|
|
||||||
> `git diff 0a3c48fc~1..0a3c48fc`
|
|
||||||
|
|
||||||
## Generation notes (non-code, verbatim)
|
|
||||||
|
|
||||||
- Pure docstring-content edits; no signatures, logic, or
|
|
||||||
non-docstring lines touched (confirmed via `git diff` review).
|
|
||||||
- reST fix patterns applied: blank-line + base-column indent
|
|
||||||
for bullet lists ("Unexpected indentation" / "Definition
|
|
||||||
list ..." / "Block quote ..."); `**bold**` in place of an
|
|
||||||
under-length section underline ("Title underline too short");
|
|
||||||
closing/escaping backticks ("Inline interpreted text ...
|
|
||||||
without end-string"); literal-block for an ASCII tree whose
|
|
||||||
`|` chars tripped "Inline substitution_reference ...".
|
|
||||||
- The pluralized-role idiom is written `\\ s` in the Python
|
|
||||||
source (so the runtime docstring holds `` `role`\ s ``); a
|
|
||||||
bare `\ ` would raise a 3.13 invalid-escape SyntaxWarning.
|
|
||||||
Verified clean via `python -W error::SyntaxWarning -c
|
|
||||||
"import tractor"`.
|
|
||||||
- Verification gate: `sphinx-build` warning count 24 -> 2
|
|
||||||
(subagent, docstrings only) -> 0 after the orchestrator added
|
|
||||||
the `NEWS.rst` blank-line fix; rendered-HTML spot-check
|
|
||||||
confirmed no literal `\ s` leak; `ruff check` clean.
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
session: 638d5a98-abd2-4217-a447-19f2330eb334
|
|
||||||
timestamp: 2026-06-25T23:25:06Z
|
|
||||||
git_ref: 3a5cfde0
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260625T232506Z_3a5cfde0_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Follow-up to a `/code-review` of PR #460 (this branch) which
|
|
||||||
surfaced lower-severity findings in the `.. d2::` sphinx
|
|
||||||
directive. The user's driving instructions across the session,
|
|
||||||
|
|
||||||
> yup do both *at least*, then report back.
|
|
||||||
|
|
||||||
(re: "implement #7 (+ optionally #8) on the branch") and,
|
|
||||||
|
|
||||||
> well we're on a clean main so why not just do these now too?
|
|
||||||
|
|
||||||
(re: also doing #9). The three findings being addressed,
|
|
||||||
|
|
||||||
- #7: a `d2` render that is *attempted and fails* (binary
|
|
||||||
present but errors on the source) silently degraded to the
|
|
||||||
stale committed SVG and the build still exited 0,
|
|
||||||
- #8: `parallel_*_safe = True` was declared while `run()`
|
|
||||||
writes the shared output SVG during the read phase (torn
|
|
||||||
write under `sphinx-build -j`),
|
|
||||||
- #9: output keyed only by `src.stem` so two `.d2` sources
|
|
||||||
with the same stem collide.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Reworked `docs/_ext/d2diagrams.py`,
|
|
||||||
|
|
||||||
- `render_svg()` now returns a `RenderResult` tristate
|
|
||||||
(`OK` / `FELL_BACK` / `NO_OUTPUT` / `FAILED`) so `run()` can
|
|
||||||
distinguish a graceful no-binary fallback from a real render
|
|
||||||
failure; `FAILED` emits a `reporter.error` (build fails under
|
|
||||||
`-W`),
|
|
||||||
- the render writes into a sibling temp-file then `os.replace()`
|
|
||||||
(atomic) so a failed/torn render can never clobber a good
|
|
||||||
committed SVG,
|
|
||||||
- a per-build `_seen_outputs` map (reset on `builder-inited`)
|
|
||||||
errors on a same-stem output collision.
|
|
||||||
|
|
||||||
Verified: a corrupted `.d2` errors under `-W` (exit 1) with the
|
|
||||||
committed SVG byte-unchanged; the collision guard fires on a
|
|
||||||
duplicate stem; `nix run nixpkgs#d2` render path + atomic swap
|
|
||||||
leave no temp residue; `ruff` clean.
|
|
||||||
|
|
||||||
## Response summary (cont.) — Files changed
|
|
||||||
|
|
||||||
- `docs/_ext/d2diagrams.py` — tristate render result, atomic
|
|
||||||
temp-file render, output-collision guard, doc-string policy
|
|
||||||
update.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None — committed as generated (`3a5cfde0`). The user noted
|
|
||||||
follow-up refinements may come in later commits.
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5[1m]
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-06-25T23:25:06Z
|
|
||||||
git_ref: 3a5cfde0
|
|
||||||
diff_cmd: git diff 3a5cfde0~1..3a5cfde0
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output pointers (diff-ref mode)
|
|
||||||
|
|
||||||
The generated patch is committed; per the diff-ref decision
|
|
||||||
rule the verbatim content is the diff itself:
|
|
||||||
|
|
||||||
> `git diff 3a5cfde0~1..3a5cfde0 -- docs/_ext/d2diagrams.py`
|
|
||||||
|
|
||||||
## Generation notes (non-code, verbatim)
|
|
||||||
|
|
||||||
- `RenderResult` enum added; `render_svg()` return type changed
|
|
||||||
`bool -> RenderResult`. Mapping in `run()`: `FAILED` ->
|
|
||||||
`state_machine.reporter.error(...)`, `NO_OUTPUT` -> raw `.d2`
|
|
||||||
source as a `literal_block`, `OK`/`FELL_BACK` -> `image`.
|
|
||||||
- Atomicity via `tempfile.mkstemp(dir=out.parent, ...)` +
|
|
||||||
`os.replace(tmp, out)`; temp unlinked on any failure path.
|
|
||||||
- Collision guard: module-level `_seen_outputs: dict[str,str]`
|
|
||||||
keyed by output basename, cleared by a `builder-inited`
|
|
||||||
handler connected in `setup()`.
|
|
||||||
- `parallel_read_safe`/`parallel_write_safe` kept `True` but
|
|
||||||
now justified by the atomic swap (documented inline).
|
|
||||||
- Empirical verification performed before commit:
|
|
||||||
- broken `.d2` + `sphinx-build -W` -> exit 1, ERROR node
|
|
||||||
rendered, committed `actor_tree.svg` md5 unchanged,
|
|
||||||
- duplicate-stem orphan page -> "d2 output collision" error,
|
|
||||||
- `nix run nixpkgs#d2` forced re-render -> deterministic
|
|
||||||
(no git drift), no `.tmp` residue,
|
|
||||||
- `ruff check` clean.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: f4da6654-63f9-4641-b567-f200da46d697
|
|
||||||
timestamp: 2026-07-02T16:16:24Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T161624Z_65bf9df5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
> attempt to resolve
|
|
||||||
> https://github.com/goodboy/tractor/issues/474
|
|
||||||
> do it with /open-wkt.
|
|
||||||
|
|
||||||
Issue #474: "We need test suite for
|
|
||||||
`.trionics.start_or_cancel()`" — dig out the use case
|
|
||||||
from `modden` as a unit test (and provide motivation
|
|
||||||
for upstream `trio` to possibly provide better
|
|
||||||
porcelain around this case). The wrapper landed via
|
|
||||||
PR #464 with no tests.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
New `tests/trionics/test_taskc.py` (9 tests) covering
|
|
||||||
`start_or_cancel()`, authored in wkt
|
|
||||||
`start_or_cancel_tests_474`.
|
|
||||||
|
|
||||||
Behaviour was first probed empirically (trio 0.29):
|
|
||||||
the lossy `RuntimeError('child exited without calling
|
|
||||||
task_status.started()')` only fires when the child
|
|
||||||
exits pre-`.started()` WITHOUT propagating the ambient
|
|
||||||
`Cancelled` — i.e. when the child (or lib code it
|
|
||||||
calls) absorbs the cancel in a graceful-teardown
|
|
||||||
pattern; a well-behaved child surfaces `Cancelled`
|
|
||||||
straight out of `.start()`. The `modden`
|
|
||||||
`progman.open_wks()` use case was reconstructed from
|
|
||||||
`modden/runtime/progman.py` accordingly.
|
|
||||||
|
|
||||||
Tests (each `use_start_or_cancel` parametrization also
|
|
||||||
pins upstream trio's current lossy behaviour as
|
|
||||||
wart-documentation):
|
|
||||||
|
|
||||||
- `test_sibling_err_not_masked_by_startup_rte` — the
|
|
||||||
`modden` case: sibling error OOB-cancels the shared
|
|
||||||
nursery scope; with the wrapper ONLY the root-cause
|
|
||||||
`ValueError` escapes; bare `.start()` adds the lossy
|
|
||||||
RTE alongside.
|
|
||||||
- `test_pure_oob_cancel_not_morphed_to_rte` — plain
|
|
||||||
ancestor `cs.cancel()`: wrapper → clean exit; bare
|
|
||||||
→ eg-wrapped RTE.
|
|
||||||
- `test_genuine_startup_rte_still_raised` — no
|
|
||||||
cancellation → protocol-bug RTE re-raised same as
|
|
||||||
bare.
|
|
||||||
- `test_childs_own_rte_never_demoted_to_cancel` — a
|
|
||||||
child's own `RuntimeError('never got started!')` /
|
|
||||||
`RuntimeError(1234)` under ambient cancel is never
|
|
||||||
demoted to `Cancelled` (exact-msg-match + str-guard
|
|
||||||
regression cover).
|
|
||||||
- `test_started_value_and_args_passthru` — happy path:
|
|
||||||
positional args, `name=`, `.started()` value.
|
|
||||||
|
|
||||||
Verified: 9/9 pass; 0 flakes across 50 hammer runs;
|
|
||||||
two impl mutations (checkpoint removed; guard relaxed
|
|
||||||
to substring match) each caught by exactly the
|
|
||||||
targeted tests; `tests/trionics/` +
|
|
||||||
`tests/test_trioisms.py` subset green (23 passed,
|
|
||||||
5 xfailed); ruff clean; 69-col style.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/trionics/test_taskc.py` — new
|
|
||||||
`start_or_cancel()` unit-test suite (gh #474).
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
Pending review — session paused pre-commit per user
|
|
||||||
deadline; nothing committed as of this entry.
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T16:16:24Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
diff_cmd: git diff main..wkt/start_or_cancel_tests_474
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output — gh #474 `start_or_cancel()` test suite
|
|
||||||
|
|
||||||
## Generated test code
|
|
||||||
|
|
||||||
> `git diff main..wkt/start_or_cancel_tests_474 -- tests/trionics/test_taskc.py`
|
|
||||||
|
|
||||||
Prose summary of the generated module
|
|
||||||
(`tests/trionics/test_taskc.py`):
|
|
||||||
|
|
||||||
- module docstring framing the `trio.Nursery.start()`
|
|
||||||
startup-cancellation wart, the wrapper's repair, and
|
|
||||||
the intent that `use_start_or_cancel=False` params
|
|
||||||
double as upstream-trio wart-documentation (break on
|
|
||||||
a trio upgrade → upstream may have shipped porcelain,
|
|
||||||
re-audit the wrapper); cites gh #474 / PR #464 and
|
|
||||||
`modden`'s `progman.open_wks()` as the source use
|
|
||||||
case.
|
|
||||||
- shared children: `absorbs_cancel_pre_started()` (the
|
|
||||||
graceful-teardown cancel-absorber which triggers the
|
|
||||||
lossy RTE path) + `raise_val_err()` (fast-erroring
|
|
||||||
sibling).
|
|
||||||
- `test_sibling_err_not_masked_by_startup_rte`
|
|
||||||
(parametrized `use_start_or_cancel`): asserts eg
|
|
||||||
contains exactly one `ValueError` and, wrapper-case,
|
|
||||||
NO residual RTE (`eg.split(ValueError)` remainder is
|
|
||||||
`None`); bare-case, the residual RTE carries trio's
|
|
||||||
exact "child exited without calling" wording.
|
|
||||||
- `test_pure_oob_cancel_not_morphed_to_rte`
|
|
||||||
(parametrized): wrapper-case runs clean and asserts
|
|
||||||
`cs.cancelled_caught`; bare-case asserts the
|
|
||||||
eg-wrapped RTE.
|
|
||||||
- `test_genuine_startup_rte_still_raised`
|
|
||||||
(parametrized): no-cancel protocol bug → RTE with
|
|
||||||
trio's wording from both call forms.
|
|
||||||
- `test_childs_own_rte_never_demoted_to_cancel`
|
|
||||||
(parametrized `rte_arg` in `'never got started!'`,
|
|
||||||
`1234`): child cancels the ambient scope then raises
|
|
||||||
its own RTE synchronously (no checkpoint between →
|
|
||||||
deterministically under-cancellation at catch time);
|
|
||||||
asserts the RTE survives with `args[0]` intact.
|
|
||||||
- `test_started_value_and_args_passthru`: `.started()`
|
|
||||||
value, positional args and the `name=` kwarg (via
|
|
||||||
`trio.lowlevel.current_task().name`) all forward.
|
|
||||||
|
|
||||||
## Non-code output (verbatim highlights)
|
|
||||||
|
|
||||||
Behaviour probe (trio 0.29, scratchpad scripts) — the
|
|
||||||
decision basis for the test shapes:
|
|
||||||
|
|
||||||
```
|
|
||||||
== B-sibling-err use_soc=False
|
|
||||||
start raised: RuntimeError('child exited without
|
|
||||||
calling task_status.started()')
|
|
||||||
top-level: ExceptionGroup([ValueError('sibling blew
|
|
||||||
up!'), RuntimeError('child exited without calling
|
|
||||||
task_status.started()')])
|
|
||||||
== B-cs-cancel use_soc=False
|
|
||||||
top-level: ExceptionGroup([RuntimeError('child
|
|
||||||
exited without calling task_status.started()')])
|
|
||||||
== B-sibling-err use_soc=True
|
|
||||||
start raised: Cancelled()
|
|
||||||
top-level: ExceptionGroup([ValueError('sibling blew
|
|
||||||
up!')])
|
|
||||||
== B-cs-cancel use_soc=True
|
|
||||||
start raised: Cancelled()
|
|
||||||
top-level: clean return
|
|
||||||
== own-rte-under-cancel (both) -> RTE('never got
|
|
||||||
started!') propagates unchanged
|
|
||||||
```
|
|
||||||
|
|
||||||
Key finding: with a WELL-BEHAVED (non-absorbing) child
|
|
||||||
an OOB ancestor cancel surfaces `Cancelled` directly
|
|
||||||
from `.start()` on trio 0.29 — the lossy RTE requires
|
|
||||||
the child to absorb its cancel pre-`.started()`, which
|
|
||||||
is what `modden`'s `open_from_wks` teardown did. Trio's
|
|
||||||
nursery-exit wait defers cancel delivery to children,
|
|
||||||
so all tested shapes are deterministic (0 flakes / 50
|
|
||||||
runs).
|
|
||||||
|
|
||||||
Mutation verification:
|
|
||||||
|
|
||||||
```
|
|
||||||
mutation 1 (checkpoint_if_cancelled removed):
|
|
||||||
FAILED test_sibling_err_not_masked_by_startup_rte[True]
|
|
||||||
FAILED test_pure_oob_cancel_not_morphed_to_rte[True]
|
|
||||||
mutation 2 (guard relaxed to 'started' substring,
|
|
||||||
isinstance dropped):
|
|
||||||
FAILED test_childs_own_rte_never_demoted_to_cancel[never got started!]
|
|
||||||
FAILED test_childs_own_rte_never_demoted_to_cancel[1234]
|
|
||||||
```
|
|
||||||
|
|
||||||
Final runs:
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/trionics/test_taskc.py: 9 passed in 0.03s
|
|
||||||
hammer: 0/50 runs failed
|
|
||||||
tests/trionics/ + tests/test_trioisms.py:
|
|
||||||
23 passed, 5 xfailed in 3.02s
|
|
||||||
```
|
|
||||||
|
|
@ -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`).
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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`).
|
|
||||||
|
|
@ -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)
|
|
||||||
```
|
|
||||||
|
|
@ -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`).
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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`).
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# AI Prompt I/O Log — claude
|
|
||||||
|
|
||||||
This directory tracks prompt inputs and model
|
|
||||||
outputs for AI-assisted development using
|
|
||||||
`claude` (Claude Code).
|
|
||||||
|
|
||||||
## Policy
|
|
||||||
|
|
||||||
Prompt logging follows the
|
|
||||||
[NLNet generative AI policy][nlnet-ai].
|
|
||||||
All substantive AI contributions are logged
|
|
||||||
with:
|
|
||||||
- Model name and version
|
|
||||||
- Timestamps
|
|
||||||
- The prompts that produced the output
|
|
||||||
- Unedited model output (`.raw.md` files)
|
|
||||||
|
|
||||||
[nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Entries are created by the `/prompt-io` skill
|
|
||||||
or automatically via `/commit-msg` integration.
|
|
||||||
|
|
||||||
Human contributors remain accountable for all
|
|
||||||
code decisions. AI-generated content is never
|
|
||||||
presented as human-authored work.
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: moc-teardown-completion-20260804
|
|
||||||
timestamp: 2026-08-04T03:03:09Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260804T030309Z_65bf9df5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Patch Tractor's `maybe_open_context()` so the final consumer waits for
|
|
||||||
resource `__aexit__()` completion and receives cleanup errors. Reuse
|
|
||||||
`outcome.Outcome` for the exit result; use the smaller mutable
|
|
||||||
`_CtxExit` holder if that makes the implementation simpler. Add and run
|
|
||||||
the relevant existing unit tests, but do not commit or push the patch.
|
|
||||||
After reviewing the result, simplify `_CtxExit` back to an optional
|
|
||||||
exception because the success outcome carries no useful value.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Added an exception-backed completion handshake between
|
|
||||||
`_Cache.run_ctx()` and the final `maybe_open_context()` consumer.
|
|
||||||
Serialized consumer registration and final teardown under the per-key
|
|
||||||
lock, preserving that lock for queued entrants. Added deterministic
|
|
||||||
regressions for normal exit, cleanup errors, cancellation interactions,
|
|
||||||
service-nursery cancellation, and teardown re-entry.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_mngrs.py` - publish and unwrap cached exit outcomes.
|
|
||||||
- `tests/test_resource_cache.py` - cover completion and cancellation.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The user directed the final simplification from `outcome.Outcome` to an
|
|
||||||
optional exception field. The patch remains uncommitted.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-04T03:03:09Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implemented cached-context exit completion in
|
|
||||||
`tractor.trionics.maybe_open_context()`.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_mngrs.py`
|
|
||||||
|
|
||||||
The generated implementation adds `_CtxExit`, whose `done` event
|
|
||||||
publishes an `outcome.Outcome[None]`. `_Cache.run_ctx()` records either
|
|
||||||
`Value(None)` or `Error(exc)` after the resource exit attempt. The final
|
|
||||||
MOC consumer signals `no_more_users`, waits for completion under a
|
|
||||||
shielded cancel scope, removes the per-key lock, and unwraps the outcome
|
|
||||||
so ordinary cleanup failures are raised at the consumer boundary.
|
|
||||||
`trio.Cancelled`, `KeyboardInterrupt`, and `SystemExit` continue through
|
|
||||||
the service task rather than being converted into regular cleanup
|
|
||||||
errors.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_resource_cache.py`
|
|
||||||
|
|
||||||
The generated regressions cover successful exit blocking, cleanup-error
|
|
||||||
delivery, final-user cancellation, cancellation combined with a cleanup
|
|
||||||
error, and service-nursery cancellation. The existing teardown re-entry
|
|
||||||
test now uses explicit events instead of a ten-second cleanup sleep and
|
|
||||||
asserts that the replacement resource is a fresh cache miss.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
`env PYTHONPATH="$PWD" /home/goodboy/repos/tractor/py313/bin/python -m pytest tests/test_resource_cache.py`
|
|
||||||
|
|
||||||
Result: `16 passed in 8.37s`.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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`
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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`
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-22T02:25:26Z
|
|
||||||
git_ref: eb3c99c9
|
|
||||||
scope: config
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260822T022526Z_5562fd9a_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Perform a full Tractor repository scan for related `ai.skillz` work,
|
|
||||||
then correct the run-tests landing branch, prune unrelated `.gitignore`
|
|
||||||
additions, preserve only the focused migration, and provide canonical
|
|
||||||
deployment commands.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Audited all local branches, worktrees, affected-path history, deployment
|
|
||||||
state, canonical skill dependencies, and current Tractor harness behavior.
|
|
||||||
Corrected the local test reference where it overstated cleanup safety or
|
|
||||||
omitted current environment, platform, debugger, timeout, and CI details.
|
|
||||||
Narrowed the correction commit to three managed `run-tests` deployment
|
|
||||||
blocks. A later dedicated commit records the complete generated `ai.skillz`
|
|
||||||
deployment state.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `.claude/skills/run-tests/test-harness-reference.md` - correct the
|
|
||||||
project-specific test and cleanup contract.
|
|
||||||
- `.gitignore` - narrow the correction commit before the later dedicated
|
|
||||||
deployment-state expansion.
|
|
||||||
- `ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.md` - record
|
|
||||||
the migration review provenance.
|
|
||||||
- `ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.raw.md` -
|
|
||||||
preserve the unedited response record.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human required an existing-work scan after duplicate implementation
|
|
||||||
was discovered, approved correcting the landing branch during PR #481
|
|
||||||
review, and directed removal or reconciliation of unrelated ignore rules.
|
|
||||||
During PR #510 review, the human required the stackscope and shared-memory
|
|
||||||
safety clarifications and immutable provenance pointers before landing. No
|
|
||||||
direct source-line edits were made by the human. Copilot review then prompted
|
|
||||||
the human to require explicit canonical deployment instructions and clarify
|
|
||||||
the later `.gitignore` expansion.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-22T02:25:26Z
|
|
||||||
git_ref: eb3c99c9
|
|
||||||
diff_cmd: git diff eb3c99c9^..eb3c99c9
|
|
||||||
---
|
|
||||||
|
|
||||||
Audit Tractor's repository-wide `ai.skillz` state and correct the
|
|
||||||
`wkt/ai_skillz_run_tests_landing` migration before landing it.
|
|
||||||
|
|
||||||
> `git diff eb3c99c9^..eb3c99c9 -- .claude/skills/run-tests/test-harness-reference.md`
|
|
||||||
|
|
||||||
The harness reference is corrected against current Tractor behavior:
|
|
||||||
environment overrides, CI/platform matrices, randomized registry
|
|
||||||
addresses, interactive debugger capture, timeout mechanisms, stackscope
|
|
||||||
artifacts, reaper platform limits, dry-run expansion, and exact
|
|
||||||
change-to-test mappings.
|
|
||||||
|
|
||||||
> `git diff eb3c99c9^..eb3c99c9 -- .gitignore`
|
|
||||||
|
|
||||||
Unrelated Taken and broad OpenCode command ignore additions are removed;
|
|
||||||
only the managed canonical `run-tests` link and command blocks remain in
|
|
||||||
this migration.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue