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
//! The cross-invocation timeout budget for `tm wait` (#5843).
//!
//! Why: one `tm wait` invocation must return before the harness ceiling, but
//! the HARD timeout it enforces has to span every re-run — otherwise an agent
//! re-issuing the command resets its own deadline and the wait never expires.
//! The budget therefore lives in a small file keyed by the condition, not in
//! process memory.
//! What: [`Budget`] is the record (`started_at` / `updated_at` / `polls` /
//! `timeout_s`); [`path_for`] derives its file name from the condition spec;
//! [`load_or_start`] resumes an existing budget, starts a fresh one, or
//! ABANDONS a stale one; [`save`] and [`clear`] persist and remove it. Every
//! function takes `now` so the tests never depend on the wall clock.
//! Test: `budget_*` in the sibling `tests.rs`.
use ;
use ;
use Context as _;
use ;
/// A condition's timeout budget, shared across re-runs.
///
/// Why: `remaining()` is what the "pending" status line reports, and it is the
/// only reason an agent can tell a wait that is progressing from one that is
/// about to expire.
/// What: when the wait began, when it was last polled, how many polls have
/// happened, and the hard timeout in force.
/// Test: `budget_resumes_across_invocations` proves `remaining` survives a
/// re-run; `loop_returns_timeout_when_budget_exhausted` proves `expired` ends
/// the wait once the budget reaches zero.
pub
/// Default directory holding budget files.
///
/// Why: a temp dir is the right lifetime — a budget is meaningless once the
/// machine reboots, and nothing here is worth backing up.
/// What: `$TMPDIR/tm-wait`.
/// Test: `path_for_is_stable_and_discriminating`.
pub
/// Derive the budget file path for a condition spec.
///
/// Why: two invocations naming the same condition must land on the same file,
/// and a spec can contain path separators and spaces that cannot go in a file
/// name. Hashing alone would make the directory unreadable, so the name keeps a
/// sanitised, truncated prefix AND a hash of the FULL spec — truncation can
/// collide, the hash cannot be read, and together they are both legible and
/// discriminating.
/// What: `<dir>/<sanitised spec, ≤64 chars>-<16 hex of hash>.json`.
/// Test: `path_for_is_stable_and_discriminating`.
pub
/// Resume, start, or abandon-and-restart the budget for a condition.
///
/// Why: three cases have to be told apart. A prompt re-run RESUMES (that is the
/// whole point of the file). A changed `--timeout` RESTARTS, because the agent
/// changed the terms of the wait. And a budget nobody has touched for a long
/// time is ABANDONED — otherwise a leftover file from yesterday's identical
/// wait would make today's first invocation report an instant timeout.
/// What: reads `path`; returns the stored budget when its spec and timeout
/// match and it was updated within `abandon_after` seconds, else a fresh one.
/// An unreadable or corrupt file is treated as absent, never as an error — the
/// budget is an optimisation for the deadline, not a source of truth to fail on.
/// Test: `budget_resumes_across_invocations`, `budget_restarts_on_timeout_change`,
/// `budget_abandons_a_stale_record`, `budget_ignores_corrupt_file`.
pub
/// Persist a budget, creating the state directory when needed.
///
/// Why: the next invocation can only resume what this one wrote.
/// What: creates the parent directory, then writes the record as JSON.
/// Test: `budget_resumes_across_invocations`.
pub
/// Remove a budget file once the wait reaches a terminal outcome.
///
/// Why: leaving it behind means the next identical wait inherits an exhausted
/// deadline. Best-effort on purpose — a wait that succeeded must not fail
/// because a temp file could not be unlinked.
/// What: deletes `path`, ignoring any error.
/// Test: `budget_clear_removes_the_file`.
pub
/// Current unix time in seconds.
///
/// Why: one conversion point, so every caller agrees on the epoch.
/// What: seconds since the unix epoch; 0 if the clock predates it.
/// Test: covered indirectly by the live invocation.
pub