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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! The wedge watchdog: fail a run that no system can ever look at again.
//!
//! Every non-terminal agent rests between ticks holding exactly one phase
//! marker, and each marker is a claim some system has on it: `ReadyToInfer` is
//! dispatch's, `AwaitingInference` is the inference collector's, `FanOutWaiting`
//! is the fan-out collector's, and so on. The marker is what makes the agent
//! reachable. An agent that is non-terminal and holds *none* of them is in a
//! state no query matches, so nothing will ever touch it again, and it stays
//! `running` in `meta.json` for the life of the daemon.
//!
//! That is not hypothetical. `PipelineWorld` already logs "a pipeline system
//! panicked outside any agent's scope; the daemon survived (an agent may be
//! wedged - cancel it via `lev cancel <run-id>`)": the runtime knows it can
//! strand a run and asks a person to clean up. Issue #202 is what happens when
//! the "person" is an unattended harness. It counted the run as occupying a slot
//! for ever, and its factory ran down to zero free slots over a few hours.
//!
//! ## Why this cannot produce a false positive
//!
//! The trigger is structural, not temporal. It is not "this run looks old" -
//! that is the shape of the misdiagnosis in issues #184, #189, #190 and #197,
//! where a fresh `updated_at`, a bare `waiting`, and a `pid` field each got read
//! as evidence they were never able to give. It is "the pipeline's own
//! invariants say this state cannot exist", and the timeout only absorbs the
//! transient windows inside a tick.
//!
//! Those invariants hold on both sides. Every site that removes a phase marker
//! either inserts a successor or sets a terminal status, and `spawn_agent_seeded`
//! always lands `Active + ReadyToInfer`, so no ordinary path arrives here.
//!
//! What is *not* touched, and why:
//!
//! - A long inference holds `AwaitingInference` and `InFlightWork`, and is
//! bounded twice over besides (the provider's job timeout, and the lane
//! supervisor that turns a dead task into an ordinary error outcome). A
//! fifteen-minute call is never a candidate.
//! - A full inference pool leaves the agent `ReadyToInfer` with a
//! [`DispatchStall`](super::DispatchStall). That is backpressure working as
//! designed, and issue #190's watchdog already declines to fail it.
//! - A tool batch holds `AwaitingTools` and is deliberately unbounded: it may
//! park off-lane on a tool approval, an `ask_user`, or a `wait_for_agent` that
//! ends only when some other run does. Any clock-based rule would have to
//! guess a bound here. This one does not have to.
//! - A run blocked on a person holds an interaction marker and is left entirely
//! alone. That is issue #204's territory, and deliberately so: killing a run
//! somebody is about to answer is a worse failure than leaking a slot, and two
//! timeouts racing over one status is how #184 happened.
//! - `Paused` is skipped before the clock is even read, and loses any record it
//! was carrying, so resuming never finds a run the watchdog had started
//! counting.
//!
//! One residual class this does not cover, stated plainly: an agent that *holds*
//! a marker but is missing some component the matching system's query also
//! requires, so that query never sees it. No production path builds an agent
//! that way today. Covering it would need exactly the "looks old" reasoning the
//! rest of this module exists to avoid, so it is left uncovered rather than
//! guessed at.
//!
//! ## It composes upward
//!
//! Failing a wedged child is enough to free its parent. The fan-out collector
//! already counts an `Error` worker as finished, and a `requires_children` gate
//! releases on any terminal child, so there is no parent case to special-case.
use *;
/// An agent found in a state no system can reach, and when it was first seen
/// that way.
///
/// One field, unlike [`DispatchStall`](super::DispatchStall), which also carries
/// a freshness stamp. That record is written by the dispatch systems and read by
/// a different one, so it has to cope with its writer going away. This one has a
/// single owner: the watchdog inserts it when the condition holds, keeps the
/// original `since` while it keeps holding, and removes it the moment the agent
/// becomes reachable again. There is nothing to go stale.
/// How long an agent may sit unreachable before the run is failed.
///
/// A world resource rather than a constant because the daemon serves it from
/// `[limits] wedge_timeout_secs`. Zero disables the watchdog.
;
/// Default grace period before an unreachable agent fails its run: `0`, meaning
/// the watchdog is off unless an operator turns it on.
///
/// Off by default because this fails runs, and a daemon that starts killing work
/// after an upgrade nobody asked for is a worse outcome than the leak it
/// prevents. `300` is the value to set once you want it: five minutes is ten of
/// the daemon's thirty-second re-drives, the same span `dead_cycles_before_relief`
/// already treats as "long enough to act on".
pub const DEFAULT_WEDGE_TIMEOUT_SECS: u64 = 0;
/// Query filter: the agent holds nothing that will cause any system to look at
/// it again.
///
/// Every entry is a component whose *presence* means some system has this agent
/// queued. This is the load-bearing list in the module, and the one thing here
/// that needs maintaining: **a new phase marker must be added here**, or an agent
/// resting on it will be mistaken for an unreachable one. The table-driven test
/// at the bottom of this file is what catches that.
///
/// Note that [`PipelineWorld::fingerprint`](crate::world::PipelineWorld) counts a
/// subset of these markers for a different purpose (deciding whether a tick
/// changed anything). The two lists answer different questions and are not
/// interchangeable.
///
/// Components that are per-agent *data* rather than a claim on the agent -
/// `StageCursor`, `StageProgress`, `DynamicTools`, the auto-approve markers, and
/// `DispatchStall` itself - are deliberately absent. Their presence says nothing
/// about whether anything is going to run.
pub type Unreachable = ;
/// Wedge watchdog: fail any non-terminal agent that has been unreachable for
/// longer than [`WedgeTimeout`].
///
/// See the module documentation for why this is safe. In short: an agent matches
/// [`Unreachable`] only in a state the rest of the pipeline guarantees it never
/// leaves an agent in, so anything that matches is already lost.
/// Whether an agent has stopped for good. Terminal agents are left to the host,
/// which reaps them once their state has been persisted and reported.