- Plexer: always-active terminal multiplexing. NativePlexerController/ TmuxPlexerController share one interface, chosen by which env var is already set (TAI_PLEXER_SOCKET vs TMUX_PANE); NativePlexerBackend is the pty.fork()-based wrapper (raw mode, SIGWINCH resize forwarding, a Unix-socket control protocol, continuous session logging, a Ctrl+A prefix key distinct from tmux's own Ctrl+B). Auto-wraps new interactive shells via a .bashrc hook mirroring the existing command-not-found install pattern, gated on $TMUX/$TAI_PLEXER_ACTIVE/ a $HOME/.tai-no-plexer bypass. New plexer_send/read/type/status tools, env-gated on a detected controller. - tests/container.py + docker-compose.yml + tests/docker/Dockerfile: a disposable podman/docker image for the tests that must never touch a real machine (installing the plexer hook into a real ~/.bashrc, a live tmux session). Required suite (test_seal.py, test_tai.py, tests/plexer.py, tests/bashrc.py) gates the result; tests/rag.py runs as best-effort since its real-LLM dependency can't be satisfied inside a disposable container with no nested engine. - tests/bashrc.py: real end-to-end hook install + bash -i auto-wrap, anti-recursion, tmux-active skip, non-interactive skip, a live TmuxPlexerController test, and an install/uninstall round-trip. - tests/plexer.py: NativePlexerBackend under a real pty, plus a dedicated robustness pass (file permissions, >64KB payload framing, a malformed control request, a genuine self-deadlock fix in the write-to-child path). - tai_test.py: the shared async process-driving framework (pipe/pty transport, group-wide kill-and-reap, a StdinBridge for --interactive) both test files build on. - ChatClient last-resort backend: when there's no local opencode CLI but the sandbox box is already running, install opencode into it (official Node tarball, not apt -- apt's nodejs/npm pull in webpack/babel/jest as transitive deps) and route through it instead of giving up. Only reachable after the first retry round already failed everything else; never creates the box itself. TmuxPlexerController.status() fixed to use `list-panes` instead of `display-message -p`, which silently fell back to the default pane instead of erroring against a dead target. - Fix 4 pre-existing test_tai.py bugs surfaced by running it in a clean environment for the first time: two stale mock signatures (subprocess.run's stdout= kwarg, Chat.complete's deadline= kwarg), one $TMUX-dependent catalog assertion, one tty-dependent color assertion. - research/: a sourced research archive (Source Dossier/Citation Card/Conclusion tiers) backing the above design decisions, with its own audit.py consistency checker. - .gitignore: fix a blanket *.md rule that was about to silently drop the entire research/ archive from version control; add __pycache__/ and the one-off debug transcript this session started from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2.5 KiB
2.5 KiB
CPython bug reports: asyncio.wait_for() timeout does not kill the awaited process
Metadata
| Field | Value |
|---|---|
| Institute | Molodetz Institute — Research & Retrieval Division |
| Subject | async-pty-process-testing |
| Document type | Source Dossier |
| Tier | 1 |
| Source title | bugs.python.org msg233486, and companion msg357930 |
| Source URL | https://bugs.python.org/msg233486 |
| Source publisher / author | Python core development bug tracker (bugs.python.org / BPO) |
| Source type | other (official bug tracker message) |
| Source date (published) | Unknown — not stated |
| Date researched | 2026-10-09 |
| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor |
| Research method | web search synthesis |
| Confidence | High |
| Status | Active |
Summary
Official CPython bug tracker messages confirming that asyncio.wait_for() timing out does not, by itself, terminate the coroutine/process it was waiting on — the awaited operation can continue running in the background after the TimeoutError is raised to the caller.
Key findings
- A bug report describes
wait_for()timing out while the process it was waiting on continued living in the background — the timeout only stops waiting, it does not reach into and stop the underlying operation. - This is documented as a known, discussed behavior in CPython's own bug tracker (two related messages), not a one-off misunderstanding.
Pros / strengths
- Primary-source, from the language's own bug tracker — about as authoritative as a finding in this subject gets.
- Directly actionable: it tells you precisely what
wait_for()does not do for you, closing a dangerous assumption gap.
Cons / limitations / caveats
- Only the two referenced messages were reviewed, not the full bug thread's eventual resolution or whether later Python versions changed this behavior.
Relevance to tai
This is the specific reason tai_test.py.TestCase.execute() never treats a caught asyncio.TimeoutError (from the asyncio.wait_for(self.run(), timeout=self.timeout) wrapping every test) as sufficient cleanup on its own. The finally block unconditionally calls await proc.terminate() for every spawned process, precisely because this source confirms the timeout itself does nothing to stop the process — only the explicit terminate() call (with its own killpg escalation and reap) does.
Changelog
| Date | Change |
|---|---|
| 2026-10-09 | Initial dossier created. |