1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// SPDX-License-Identifier: AGPL-3.0-only
//! The process-global child reaper.
//!
//! `waitpid(-1)` is process-global — it reaps *any* child (including
//! `PR_SET_CHILD_SUBREAPER` orphans), so two threads each calling it would steal
//! each other's children (the robbed one then waits forever for an exit another
//! thread already collected).
//!
//! The fix is **dispatch by pid**: exactly one place drains `waitpid(-1)` and
//! routes each reaped pid to the owning `Supervisor`'s channel, so any number of
//! Supervisors run **concurrently** without stealing from each other.
//!
//! Two operations, both under a global pid→route registry mutex:
//! * [`spawn_tracked`] forks the child **under the lock**, then registers its
//! pid → the owner's reap channel — so registration is atomic with the fork
//! and the reaper can never `waitpid` a not-yet-registered child.
//! * [`reap_and_dispatch`] (called from each Supervisor's tick) drains
//! `waitpid(-1, WNOHANG)` and sends each reaped pid to its owner; an **unowned**
//! pid (an adopted orphan, or a *foreign* child such as an MCP-server / `exec`
//! process that reaps itself) is simply dropped — already reaped, no owner.
//!
//! There is **no dedicated reaper thread**: reaping happens only while a
//! Supervisor is active (its 200 ms tick drives it). That is deliberate — a
//! continuous `waitpid(-1)` would steal the children of components that
//! spawn-and-wait their own (the `exec` self-tool, the MCP client).
//!
//! **What `waitpid(-1)` reaps (the real coexistence contract).** Because it is
//! process-global, `reap_and_dispatch` reaps *every* exited child in the process,
//! not just tracked ones — including a daemon's long-lived MCP-server children, a
//! warm session's subagent, and adopted orphans — and it does so from **whichever
//! supervisor happens to tick**, possibly concurrently with the main thread. That
//! is safe **not** because these never overlap (in the daemon they do, once a
//! served async run is in flight) but because every such component detects its
//! child's death via its own channel (MCP stdout EOF, the warm/async `AgentMsg`
//! channel) and its `Drop` tolerates `ECHILD` — none of them needs the reaped
//! exit *status*, which is what `waitpid(-1)` consumes. The one component that
//! *does* consume a child's status, the `exec` tool, runs only on a subagent's
//! single agentic-loop thread where no reactor ticks concurrently. A foreign
//! `child.wait()` is `waitpid(specific_pid)` and so can never steal a *tracked*
//! supervised child (a different, still-live pid).
use crate;
use crateSubagent;
use HashMap;
use io;
use Sender;
use ;
/// pid → the owning Supervisor's reap channel. Holds only LIVE (unreaped)
/// supervised pids; an entry leaves when its pid is reaped (dispatched) or when
/// its handle is dropped unreaped ([`deregister`]).
static ROUTES: =
new;
/// Spawn a supervised child and register its pid → `reap_tx` **atomically with
/// the fork** (both under the routes lock), so the reaper can never `waitpid` a
/// child before it is registered. `spawn_fn` does the fork and returns the
/// [`Subagent`] whose `pid()` is the registry key.
///
/// The lock is held across all of `spawn_fn` — the fork, the first-frame payload
/// write, and the reader-thread spawn — so concurrent supervisors briefly
/// serialize on each spawn. The hold is bounded by child startup (the child
/// drains its stdin pipe within a few ms of `exec`), never by the length of a
/// run, so spawn contention stays in the millisecond range.
/// [`spawn_tracked`] for a plain [`std::process::Child`] — an instance-tier
/// child, which is a full daemon with no control channel. Same contract: the
/// fork happens under the routes lock so the reaper can never `waitpid` the pid
/// before it is registered.
/// Drain `waitpid(-1, WNOHANG)` and dispatch each reaped pid to its owning
/// Supervisor. Unowned pids (orphans / foreign self-reaping children) are
/// dropped. Called from each Supervisor's tick; the lock keeps the single
/// `waitpid(-1)` serialized across concurrent Supervisors.
/// Drop a pid's route without reaping it — for a [`Subagent`] handle dropped
/// before the reaper dispatched its exit (an abandoned run, which then reaps the
/// child itself). Harmless if the pid is absent (a foreign / already-reaped pid).