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
//! Finalization-gate lifecycle state, in one place [dirge-5mtx.5].
//!
//! Every finalization gate in [`super::run::poll_finalization_follow_up`] needs
//! some per-run state to stop it re-firing. These used to be nine separate
//! `&mut` parameters threaded through a signature that carried
//! `#[allow(clippy::too_many_arguments)]` to stay quiet. Collecting them here
//! is not only tidying: it puts every gate's lifecycle next to every other
//! one, which is what makes the cost-ceiling / re-fire-guard distinction below
//! visible at all.
//!
//! # Why the counters exist
//!
//! Gate predicates evaluate over `new_messages`, which ACCUMULATES across
//! re-entries within a single finalization sequence. A condition that was true
//! once therefore stays true, so a gate with no counter would fire on every
//! pass. The counters are what stop that.
//!
//! # Two kinds of bound — do not confuse them
//!
//! Each field is labelled below. The distinction matters because it decides
//! what is safe to change:
//!
//! - **Cost ceiling** — bounds spend (LLM calls, tokens, wall time). Removing
//! or raising one costs real money on every run that hits it, and removing
//! it entirely admits an unbounded loop. These stay regardless of how good
//! the predicates get.
//! - **Re-fire guard** — exists only because the predicate cannot tell "this
//! happened" from "this happened and I already reacted". A sufficiently
//! state-derived predicate would subsume it. Relaxing one wastes a
//! round-trip at worst.
//!
//! Note that relaxing a re-fire guard was **descoped** from dirge-5mtx.5 on
//! the evidence: run-to-run variance on the A/B scenarios is ~2x, so "this
//! budget can be relaxed without hurting anything" is not distinguishable from
//! noise at any sample size worth paying for. The labels are recorded so the
//! question can be asked properly later, not because the answer is known. See
//! `docs/verification-discipline.md` for the measurement protocol.
use GateMode;
/// Per-run lifecycle state for the finalization gates.
///
/// Constructed once per `run_loop` and passed by `&mut` to every finalization
/// poll, so the counters persist across re-entries within a run and reset
/// between runs.
/// Read-only inputs the finalization gates consult but do not own.
///
/// Separate from [`GateStates`] because these are borrowed per-poll rather
/// than per-run: the code-review baseline is captured at run start and the
/// open-issues trio is resolved from config and the session.