You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Savings accounting credits rtk with the agent's own head/tail filtering — repro: 2.1M tokens claimed '100% saved' vs ~195 real (>10,000×); likely root cause of #2762 #2805
TL;DR — this is a hard one, please read it all the way through
rtk's savings accounting credits rtk with filtering the agent already did itself. One sandboxed command on develop (31f9d43) reproduces a >10,000× overstatement: rtk gain reports 2.1M tokens saved (100.0%) for a command whose real marginal saving is ~195 tokens.
We're raising this assuming good faith all the way down — this looks like a classic Goodhart trap (the metric was easy to record, so it became the product), not anything intentional. But the consequence is bigger than a bug: the headline numbers — the README "Token Savings (30-min Claude Code Session)" table, the -99% site demo, and every rtk gain screenshot — inherit this baseline and are not defensible in their current form. Fixing this properly means fixing the accounting and remeasuring, then updating README.md and the marketing copy on rtk-ai.app to match what rtk actually delivers. We think there is a real, honest value prop underneath (details at the bottom), but it is much smaller than currently claimed, and the current claims are costing you trust with exactly the users who measure.
This is very likely the root cause of #2762 ("~90M tokens saved (100%), mostly rtk read, 0% reproducible compression").
Real marginal saving: 398 − 203 ≈ 195 tokens. Claimed: ~2,072,000. The agent was never going to read the other 99,980 lines — its own head -20 guaranteed that before rtk was involved.
rtk read reads the entire file (read.rs#L25), applies the line window only to the output side (read.rs#L67), then tracks the full file content as the baseline (read.rs#L79-L84).
The tracking record labels the counterfactual cat <file> (read.rs#L80) — a command the caller never issued. The real command (head -20 …) is not stored, so rtk gain --history cannot be audited against what actually ran. That's why rtk gain reports unreproducible savings — read/grep/tail show 0% compression #2762 is stuck at "needs-reproduction": the books don't contain the information needed to reproduce them.
Same flaw, second path: pipes
Pipes rewrite left-only — git log | head -20 → rtk git log | head -20 (registry.rs#L544). The wrapped rtk process can't see its downstream consumer, so it records full-raw vs full-filtered (e.g. git.rs#L201-L210) while the agent's own | head -20 bounds what reaches the model either way. Recorded savings are unbounded; real marginal savings ≈ 0.
This generalizes: coding agents rarely run raw commands. They self-limit with | head, | tail, -n 10, --stat, rg -m — and rtk's accounting books that pre-existing filtering as rtk savings whenever a rewrite fires.
The same repro also explains the drift users report
In the reproduction above, the agent asked for 20 lines. rtk delivered 10 plus a [99990 more lines] marker:
The agent's mental model of the file no longer matches the file. Line offsets are wrong, edit anchors fail to match, and the model re-reads or retries — extra turns. This is the drift benchmark runners and real users keep describing: rtk filters content before (and differently than) the AI intended it to be filtered, on top of filtering the AI had already done deliberately. Each recovery turn costs a full context re-read plus output tokens, which can exceed an entire session's genuine rtk savings — and because the re-runs also pass through rtk, they get booked as additional savings. The dashboard improves as the experience degrades; the metric can't see its own failure mode.
Why this reaches the headline claims, not just rtk gain
The README table ("Token Savings (30-min Claude Code Session)", −80%) counts cat/read (40,000), grep/rg (16,000), and ls/tree (2,000) — 58,000 of the 118,000-token baseline — for operations Claude Code performs via its built-in Read/Grep/Glob tools, which bypass the hook entirely (per the README's own scope note). The remaining rows assume raw, unpiped commands, which is not how agents behave.
The -99% site demo is the all-tests-pass cargo test case, where output collapses to one line — the case where output detail matters least.
"Zero token overhead" for the hook doesn't count the ~5.2 KB RTK.md (~1,300 tokens) imported into every session via @RTK.md in CLAUDE.md.
We'd respectfully suggest that patching the code without remeasuring and updating the README table and rtk-ai.app copy would leave the project in the same position with the next user who measures.
What survives — the honest value prop
Genuine, defensible wins exist and are worth marketing as measured:
Verbose failure-heavy output (test runners, build logs, git ceremony) really does compress, against an honest baseline of what the agent's own command would have emitted.
In this very repro, rtk's window beat native head -20 by ~49% (203 vs 398 tokens) — that's a real number, just 4 orders of magnitude smaller than the booked one.
An rtk-off vs rtk-on A/B over real sessions (ccusage-based — cc_economics already has the plumbing) would produce numbers you can defend.
Suggested remediation
Baseline = output of the caller's actual command: window the raw content before tracking, and record the true counterfactual (head -N <file>, not cat <file>). Minimal validated patch below — accounting only, zero change to displayed output; full suite green (2,363 passed; one pre-existing flaky dotnet_trx mtime test, fails intermittently on unmodified HEAD too).
Pipe rewrites: the hook knows the original command and the surviving downstream filter — mark those records non-attributable (or pass the original command through, e.g. an env var) instead of booking full-raw baselines.
Audit the other ~100 timer.track call sites for the same full-raw-baseline pattern.
Preserve literal head/tail semantics on rewrite (the agent asked for 20 lines; delivering 10 + marker is where the drift starts). Or don't rewrite explicit line-window commands at all — the agent has already stated its filtering intent.
Remeasure with rtk-off vs rtk-on session A/Bs and update the README savings table and rtk-ai.app claims to the measured numbers.
Validated minimal patch for the read path (accounting only)
diff --git a/src/cmds/system/read.rs b/src/cmds/system/read.rs
index 141d55a..87c76c5 100644
--- a/src/cmds/system/read.rs+++ b/src/cmds/system/read.rs@@ -76,15 +76,48 @@ pub fn run(
};
let shown = never_worse(&raw, &rtk_output);
print!("{}", shown);
+ // Savings baseline: what the caller's own command would have emitted.+ // A `head -N`/`tail -N` rewritten to a windowed read was never going to+ // emit past its window; counting the whole file books savings the caller+ // already had without rtk (#2762).+ let baseline = line_window_baseline(&raw, max_lines, tail_lines);
timer.track(
- &format!("cat {}", file.display()),+ &baseline_cmd(file, max_lines, tail_lines),
"rtk read",
- &raw,+ &baseline,
shown,
);
Ok(())
}
+/// The native command a windowed read replaces, for honest tracking labels.+fn baseline_cmd(file: &Path, max_lines: Option<usize>, tail_lines: Option<usize>) -> String {+ match (tail_lines, max_lines) {+ (Some(n), _) => format!("tail -{} {}", n, file.display()),+ (None, Some(n)) => format!("head -{} {}", n, file.display()),+ (None, None) => format!("cat {}", file.display()),+ }+}++/// Literal head/tail semantics — the output the native command would produce.+/// Mirrors `apply_line_window`'s precedence (tail wins) but without rtk's+/// smart-truncation markers, since the baseline must not include rtk output.+fn line_window_baseline(+ content: &str,+ max_lines: Option<usize>,+ tail_lines: Option<usize>,+) -> String {+ match (tail_lines, max_lines) {+ (Some(n), _) => {+ let lines: Vec<&str> = content.lines().collect();+ let start = lines.len().saturating_sub(n);+ lines[start..].join("\n")+ }+ (None, Some(n)) => content.lines().take(n).collect::<Vec<_>>().join("\n"),+ (None, None) => content.to_string(),+ }+}+
pub fn run_stdin(
level: FilterLevel,
max_lines: Option<usize>,
@@ -191,6 +224,40 @@ mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
+ #[test]+ fn test_baseline_excludes_lines_past_head_window() {+ // 100-line file windowed to 5 lines: the baseline is the 5 lines the+ // caller's `head -5` would have emitted, not the whole file (#2762).+ let content: String = (1..=100).map(|i| format!("line {}\n", i)).collect();+ let baseline = line_window_baseline(&content, Some(5), None);+ assert_eq!(baseline.lines().count(), 5);+ assert!(baseline.starts_with("line 1"));+ assert!(baseline.ends_with("line 5"));+ }++ #[test]+ fn test_baseline_excludes_lines_before_tail_window() {+ let content: String = (1..=100).map(|i| format!("line {}\n", i)).collect();+ let baseline = line_window_baseline(&content, None, Some(3));+ assert_eq!(baseline.lines().count(), 3);+ assert!(baseline.starts_with("line 98"));+ assert!(baseline.ends_with("line 100"));+ }++ #[test]+ fn test_baseline_unwindowed_is_full_content() {+ let content = "a\nb\nc\n";+ assert_eq!(line_window_baseline(content, None, None), content);+ }++ #[test]+ fn test_baseline_cmd_reflects_original_command() {+ let f = Path::new("app.log");+ assert_eq!(baseline_cmd(f, Some(20), None), "head -20 app.log");+ assert_eq!(baseline_cmd(f, None, Some(50)), "tail -50 app.log");+ assert_eq!(baseline_cmd(f, None, None), "cat app.log");+ }+
#[test]
fn test_read_rust_file() -> Result<()> {
let mut file = NamedTempFile::with_suffix(".rs")?;
Environment: built from develop @ 31f9d43, cargo 1.95.0, Linux (WSL2). Repro uses RTK_DB_PATH so it never touches a real tracking DB.
Happy to turn the patch into a PR if you want it, but the accounting fix is the easy part — the ask here is the remeasurement and the README/site update. We say all this as people who want a tool like rtk to exist: the honest number is worth more than the big one.
TL;DR — this is a hard one, please read it all the way through
rtk's savings accounting credits rtk with filtering the agent already did itself. One sandboxed command on
develop(31f9d43) reproduces a >10,000× overstatement:rtk gainreports 2.1M tokens saved (100.0%) for a command whose real marginal saving is ~195 tokens.We're raising this assuming good faith all the way down — this looks like a classic Goodhart trap (the metric was easy to record, so it became the product), not anything intentional. But the consequence is bigger than a bug: the headline numbers — the README "Token Savings (30-min Claude Code Session)" table, the
-99%site demo, and everyrtk gainscreenshot — inherit this baseline and are not defensible in their current form. Fixing this properly means fixing the accounting and remeasuring, then updating README.md and the marketing copy on rtk-ai.app to match what rtk actually delivers. We think there is a real, honest value prop underneath (details at the bottom), but it is much smaller than currently claimed, and the current claims are costing you trust with exactly the users who measure.This is very likely the root cause of #2762 ("~90M tokens saved (100%), mostly
rtk read, 0% reproducible compression").Reproduction (sandboxed,
develop@ 31f9d43)Real marginal saving: 398 − 203 ≈ 195 tokens. Claimed: ~2,072,000. The agent was never going to read the other 99,980 lines — its own
head -20guaranteed that before rtk was involved.Root cause
head -N <file>→rtk read <file> --max-lines N(registry.rsrewrite_line_range).rtk readreads the entire file (read.rs#L25), applies the line window only to the output side (read.rs#L67), then tracks the full file content as the baseline (read.rs#L79-L84).cat <file>(read.rs#L80) — a command the caller never issued. The real command (head -20 …) is not stored, sortk gain --historycannot be audited against what actually ran. That's whyrtk gainreports unreproducible savings —read/grep/tailshow 0% compression #2762 is stuck at "needs-reproduction": the books don't contain the information needed to reproduce them.Same flaw, second path: pipes
Pipes rewrite left-only —
git log | head -20→rtk git log | head -20(registry.rs#L544). The wrapped rtk process can't see its downstream consumer, so it records full-raw vs full-filtered (e.g.git.rs#L201-L210) while the agent's own| head -20bounds what reaches the model either way. Recorded savings are unbounded; real marginal savings ≈ 0.This generalizes: coding agents rarely run raw commands. They self-limit with
| head,| tail,-n 10,--stat,rg -m— and rtk's accounting books that pre-existing filtering as rtk savings whenever a rewrite fires.The same repro also explains the drift users report
In the reproduction above, the agent asked for 20 lines. rtk delivered 10 plus a
[99990 more lines]marker:The agent's mental model of the file no longer matches the file. Line offsets are wrong, edit anchors fail to match, and the model re-reads or retries — extra turns. This is the drift benchmark runners and real users keep describing: rtk filters content before (and differently than) the AI intended it to be filtered, on top of filtering the AI had already done deliberately. Each recovery turn costs a full context re-read plus output tokens, which can exceed an entire session's genuine rtk savings — and because the re-runs also pass through rtk, they get booked as additional savings. The dashboard improves as the experience degrades; the metric can't see its own failure mode.
Why this reaches the headline claims, not just
rtk gaincat/read(40,000),grep/rg(16,000), andls/tree(2,000) — 58,000 of the 118,000-token baseline — for operations Claude Code performs via its built-in Read/Grep/Glob tools, which bypass the hook entirely (per the README's own scope note). The remaining rows assume raw, unpiped commands, which is not how agents behave.-99%site demo is the all-tests-passcargo testcase, where output collapses to one line — the case where output detail matters least.@RTK.mdin CLAUDE.md.rtk gainreports unreproducible savings —read/grep/tailshow 0% compression #2762's signature (16rtk readcalls → ~90M tokens "saved (100%)") is exactly what windowed reads of large files produce under this accounting.We'd respectfully suggest that patching the code without remeasuring and updating the README table and rtk-ai.app copy would leave the project in the same position with the next user who measures.
What survives — the honest value prop
Genuine, defensible wins exist and are worth marketing as measured:
head -20by ~49% (203 vs 398 tokens) — that's a real number, just 4 orders of magnitude smaller than the booked one.An rtk-off vs rtk-on A/B over real sessions (ccusage-based —
cc_economicsalready has the plumbing) would produce numbers you can defend.Suggested remediation
head -N <file>, notcat <file>). Minimal validated patch below — accounting only, zero change to displayed output; full suite green (2,363 passed; one pre-existing flakydotnet_trxmtime test, fails intermittently on unmodified HEAD too).timer.trackcall sites for the same full-raw-baseline pattern.head/tailsemantics on rewrite (the agent asked for 20 lines; delivering 10 + marker is where the drift starts). Or don't rewrite explicit line-window commands at all — the agent has already stated its filtering intent.Validated minimal patch for the
readpath (accounting only)Environment: built from
develop@ 31f9d43, cargo 1.95.0, Linux (WSL2). Repro usesRTK_DB_PATHso it never touches a real tracking DB.Happy to turn the patch into a PR if you want it, but the accounting fix is the easy part — the ask here is the remeasurement and the README/site update. We say all this as people who want a tool like rtk to exist: the honest number is worth more than the big one.