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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! `tm divert bulk-read` — the cheap worker the diversion hook steers to (#6887).
//!
//! Why: when `tm hook --divert-check` blocks an oversized read, this is the
//! command it names. It reads the files itself, asks headless Claude Code Haiku
//! the agent's question about them, and prints only the answer — so the
//! session's context carries a paragraph instead of two thousand lines. The raw
//! file content goes to the child over stdin and never reaches the parent
//! session's transcript.
//!
//! What: [`run`] dispatches the `bulk-read` action through
//! [`crate::commands::divert_worker`]. There is no provider, no credential and
//! no registry in this path — owner ruling 2026-09-07 settled the worker as
//! Claude Code's own Haiku under the developer's existing login, which the
//! child inherits via `CLAUDE_CONFIG_DIR`. On any worker failure this prints
//! [`FALLTHROUGH_MARKER`] and exits [`FALLTHROUGH_EXIT`], which the hook's own
//! block reason tells the agent to read as "retry with `offset`/`limit`". That
//! distinguishable signal is the point: a bare non-zero exit would look like a
//! transient failure worth retrying, and the agent would loop.
//!
//! Each SUCCESSFUL round trip appends one line to the session's diversion
//! ledger ([`record_diversion`]) carrying the running count and the child's own
//! token and cost numbers. A bare hook block appends nothing — only real worker
//! traffic is counted, because the number is meant to answer "what did
//! diversion save", not "how often did we say no".
//!
//! #6959 adds the second half of that answer: each successful round trip also
//! appends one `divert` row to the per-session savings ledger (see
//! [`record_divert_savings`]), so the `💸` statusline segment folds real
//! diversion traffic. A fall-through writes neither record.
//! Test: the `#[cfg(test)]` suite in `divert_tests.rs`.
use ;
use Duration;
use crateDivertAction;
use crate;
/// Stdout marker meaning "no worker ran; do it yourself".
///
/// Why: the hook's block reason quotes this string verbatim, so the agent has a
/// literal to match rather than a heuristic on exit codes it cannot see. Change
/// it here and `divert_check::block_reason` must change with it — the test
/// `fallthrough_marker_matches_the_hook_reason` pins the pair.
/// What: the exact bytes printed on any worker failure.
/// Test: `fallthrough_marker_matches_the_hook_reason`.
pub const FALLTHROUGH_MARKER: &str = "divert: fall-through";
/// Process exit code accompanying [`FALLTHROUGH_MARKER`].
///
/// Why: a caller scripting around this command needs to branch without parsing
/// prose. `3` is unused by `tm`'s other exit codes (75 is daemon-unavailable,
/// 0/1/2 are the ordinary success/failure/usage set).
/// What: `3`.
/// Test: exercised by the CLI smoke in the PR body.
pub const FALLTHROUGH_EXIT: i32 = 3;
/// Prefix of the diversion ledger line.
///
/// Why (acceptance criterion 6): the line is the record, so it needs one stable
/// literal a test — and an operator — can grep for.
/// What: `"divert.diversion"`.
/// Test: `diversion_line_carries_the_count_and_the_child_usage`.
pub const DIVERSION_LOG_MARKER: &str = "divert.diversion";
/// Maximum bytes of file content sent to the worker in one call.
///
/// Why: the worker's context is finite. Truncating explicitly beats an API-side
/// error that reads as a transport failure and triggers the fall-through path
/// for a reason the operator cannot diagnose.
/// What: 400 KiB, roughly 100k tokens of source — comfortably more than the
/// files a `min_lines` threshold diverts, and bounded.
/// Test: `read_sources_truncates_past_the_budget`.
const MAX_CONTENT_BYTES: usize = 400 * 1024;
/// Dispatch a `tm divert` action.
///
/// Why: one entry point per command group, mirroring `commands::memory::run`.
/// What: currently only `bulk-read`. Prints the worker's answer on stdout and
/// exits [`FALLTHROUGH_EXIT`] when no worker could answer.
/// Test: `cli_parses_divert_bulk_read`.
pub async
/// Answer a question about `files` on the cheap worker model.
///
/// Why: this is the whole point of the feature — the expensive session never
/// sees the bytes.
/// What: reads the files, asks the headless child, prints the answer, and
/// records one ledger line. Any worker failure prints [`FALLTHROUGH_MARKER`] to
/// stdout (so the agent, which only sees stdout, can act on it) with the detail
/// on stderr, then exits [`FALLTHROUGH_EXIT`].
/// Test: `read_sources_labels_each_file`,
/// `diversion_line_carries_the_count_and_the_child_usage`.
async
/// Read every requested file into `(path, content)` pairs.
///
/// Why: the worker needs to know which bytes came from which file, and the
/// caller needs a hard failure when a named file cannot be read — a silently
/// skipped file would produce an answer about the wrong thing.
/// What: one pair per file, stopping once [`MAX_CONTENT_BYTES`] of content is
/// gathered and noting the truncation inline.
/// Test: `read_sources_labels_each_file`, `read_sources_truncates_past_the_budget`.
/// Write the diversion ledger line, and say so on stderr.
///
/// Why (acceptance criterion 6): #6873's usage ledger is not merged, so the
/// record is a line in the session's own log. Best-effort on the file — a
/// diversion that answered must not fail because a counter could not be
/// written.
/// What: increments the per-session count, emits the line at `info` (stderr, so
/// it never corrupts the answer on stdout), and appends it to the ledger file.
/// Test: `diversion_line_carries_the_count_and_the_child_usage`,
/// `record_diversion_counts_per_session`.
/// The parent session's id, when the harness supplied one.
///
/// Why (#6959): `tm divert` runs as a plain child of the session, so the
/// harness's own session id arrives here unmodified — the scrub in
/// [`crate::commands::divert_worker::NESTED_SESSION_ENV`] strips
/// [`trusty_mpm::core::savings::CLAUDE_CODE_SESSION_ID_ENV`] from the WORKER
/// this command spawns, never from this process. That is what lets a savings
/// row be attributed to the session the statusline folds for. The diversion
/// ledger line tolerates an unknown session (it is a counter, and an uncounted
/// diversion is worse than a mislabelled one), but a savings row under no id is
/// unattributable and must be declined instead.
/// What: [`trusty_mpm::core::savings::claude_code_session_id`] — the one read of
/// that variable both savings producers share since #7209.
/// Test: `parent_session_id_env_is_scrubbed_from_the_worker_only`.
/// Total bytes of file content this diversion kept out of the session.
///
/// Why: this is the "before" half of the savings figure, and it counts the bytes
/// actually gathered rather than each file's size on disk — a read truncated at
/// [`MAX_CONTENT_BYTES`] avoided only what it gathered, so the count stays a
/// conservative floor.
/// What: the sum of every source body's length.
/// Test: `source_bytes_sums_every_file_body`.
pub
/// Record what this diversion saved, on the session's savings ledger (#6959).
///
/// Why: the diversion ledger line above answers "how often"; this answers "what
/// was it worth", and the `💸` statusline segment renders the fold. Resolving the
/// framework root HERE rather than inside the producer is what keeps the writer
/// on the same root the segment reads from — both go through the `--root` /
/// `TRUSTY_MPM_ROOT` / config chain, not the home-relative default.
/// What: prices the file-vs-summary token delta at the parent session's model —
/// which #6972 reads from the record the statusline hook wrote under the same
/// root — and appends one row. Declines quietly when there is no session id or
/// no resolvable root; never fails the diversion.
/// Test: `source_bytes_sums_every_file_body`, and the producer's own suite in
/// `savings_divert_tests.rs`.
/// Append one diversion to a session's ledger and return its running count.
///
/// Why: `tm divert` is a fresh process per diversion, so "count per session"
/// can only come from something durable. The ledger file IS the count — one
/// line per diversion — so there is no second counter to drift.
/// What: appends [`diversion_line`] to `<data_dir>/divert/<session>.log` and
/// returns the number of lines it now holds. Takes the directory as an argument
/// so the whole thing is testable against a tempdir.
/// Test: `record_diversion_counts_per_session`.
pub
/// Format one diversion ledger line.
///
/// Why: pure, so the exact literals a test (and an operator) greps for are
/// pinned in one place.
/// What: `divert.diversion count=… files=… model=… input_tokens=…
/// output_tokens=… cache_read_tokens=… cache_creation_tokens=… cost_usd=…`.
/// The cache counters are included because they dominate a Claude Code child's
/// prompt spend — a line reporting only `input_tokens` would read as far
/// cheaper than the call actually was.
/// Test: `diversion_line_carries_the_count_and_the_child_usage`.
pub