Skip to content

docs(benchmark): Terminal-Bench 2.0 study + the merged-system fifth arm - #59

Merged
OsherElhadad merged 6 commits into
mainfrom
docs/terminal-bench-final
Aug 10, 2026
Merged

docs(benchmark): Terminal-Bench 2.0 study + the merged-system fifth arm#59
OsherElhadad merged 6 commits into
mainfrom
docs/terminal-bench-final

Conversation

@OsherElhadad

Copy link
Copy Markdown
Collaborator

What

The second benchmark of the study, after SWE-bench Verified: Terminal-Bench 2.0, 89 open-ended terminal tasks, claude-code on aws/claude-sonnet-5, run live through the harness. Same four arms as SWE — baseline (off passthrough), context-guru (codesmart), headroom (hd-cache), rtk.

The claude-code trajectory parser, the cache-aware cost model and the summarizer are agent-specific rather than benchmark-specific, so every number is computed identically to the SWE arms and the two benchmarks are directly comparable in methodology. Only the Harbor dataset and jobs-root differ.

Headline (89-task raw)

dimension baseline context-guru headroom rtk
solved / 89 56 (62.9%) 58 (65.2%) 64 (71.9%) 55 (61.8%)
total billed cost $100.81 $102.55 $114.75 $118.83
cache-read tokens 216.0M 204.6M 198.0M 254.0M
cache-write tokens 4.01M 6.53M 12.37M 7.05M
mean steps (completed) 31.5 34.7 32.0 40.0

The correction, stated up front

Six baseline trials are degenerate — the baseline aborted in 2–6 steps (16–800 s) where the compaction arms ran 50–160 steps, so the cost delta on those six measures the baseline not doing the work. extract-moves-from-video alone is $24.10 of headroom's $24.65 "regression".

Recomputed over the 83 clean tasks: context-guru −9.7%, headroom −16.0%, rtk +6.4%. Both proxies save on Terminal-Bench; only rtk regresses. The comparison page opens with this caveat and the six tasks are queued for re-run at low concurrency; the tables will be regenerated then.

What the study establishes

  • The agent is ~98% cached on TB too, so cache-read is again the largest cost term — but cache-write, a rounding error on SWE-bench, is the deciding term on TB's ~1.7M-token contexts.
  • Cost tracks agent steps, r = 0.95 across every arm and both benchmarks.
  • One cache-write costs 11.5 cache-reads — so mutating already-cached content is the expensive mistake, not sending too many tokens.
  • Unique token removal is 0.02–0.13% of the billed total. Content compaction is not where the money is.
  • cache_control is metadata, not hashed content — moving a breakpoint is free and lossless.

docs/results/improvement-plan.md carries that synthesis and the prioritized work it implies.

Also in this PR

swebench.py: captures and dumps move from a fixed /tmp path to the run's own jobs-root. start_proxy unlinks the capture path, so launching any new run truncated the capture an earlier analysis was computed from — which is how a 472-request capture was lost mid-analysis. Adds the cacheonly arm, which removes no content tokens at all, so any cost delta vs off is purely breakpoint placement.

Reproduction

docs/results/REPRODUCE.md §7 (baseline: task-list generation, n=24 concurrency feasibility on this box, the timeout/budget methodology, task-metadata extraction) and §7b (the three framework-arm commands). Every credential is read from the environment at run time; nothing is embedded.

Verification

  • CGO_ENABLED=1 go build -tags cg_skeleton ./... and go test -tags cg_skeleton ./... green (docs/harness only — no library change).
  • All 89 tasks carry a scored outcome; timeouts count as reward-0 failures rather than being dropped.

Fifth arm added (2026-08-10) — supersedes #23

This branch is #23 rebased onto main after all fifteen code PRs landed, plus the re-measurement of the merged system as a fifth arm. Branch protection blocks force-push, hence the new branch.

The merged system on Terminal-Bench (81 clean tasks)

baseline context-guru (old) context-guru (merged)
solved 53 55 61
cache-write / cache-read 1.86% 2.86% 1.86%
own LLM cost $0 $2.97 $0
added latency 449.8 ms 38.5 ms
total billed $94.85 $85.72 (−9.6%) $79.32 (−16.4%)

Read the median, not the aggregate. −16.4% is dominated by path-tracing alone (−9.1% without it; an independent re-derivation with a stricter degenerate rule gave −13.7% → −2.8% on the same exclusion). The median per-task ratio is −7.8% with 49/81 tasks cheaper. Both are published because they differ by 9 points and answer different questions.

The one result needing no caveat: cache-write/cache-read returns to 1.86%, identical to baseline, where the previous arm ran 2.86%. That is the "cache-write tax" this study named as the deciding term on Terminal-Bench, eliminated — and being a ratio rather than a sum, it holds under every exclusion rule tried.

What could not be verified, stated as such

#40 (freeze TTL) is NOT exercised by this arm. All five frozen_* counters are zero, because its only callers are mask, failed_run and extract_llm — all three excluded by the chosen config. The arm is not evidence for or against #40 in either direction, and none of the cost improvement is credited to it.

cachesplit has zero legal opportunity on TB. The Claude Agent SDK never appends the git/env snapshot the CLI does, so all 73 captured requests carry 3 system blocks and no volatile-tail marker. The −34.1% split figure came from SWE-bench CLI traffic and does not transfer.

Regressions published rather than omitted

system-administration +17.2% cost and −2 solved (the only category losing both ways) · security +25.6% · fresh_input 3.8× baseline · small tasks inflating up to +311% at n=1, so size-gating remains an unclaimed win. And the honest framing: the merged arm's raw model cost nearly ties the old one and its cache-read is higher — it wins mainly by not spending $2.97 on haiku.

The methodological rule this study produced

improvement-plan.md §F-1 now records eight wrong premises, each with what was believed and what was true. Two were the orchestrator's, and one of those was an unfavourable number accepted without checking its mechanism — which reframes the bias as incuriosity rather than optimism. Nine countermeasures, the most useful being: group request lineages by append-only prefix match; instrument "did the component act?" separately from "did the metric improve?"; verify the verifier; and a sum over heterogeneous tasks can be one task.

Osher-Elhadad added 6 commits August 10, 2026 18:25
…ent plan

Second benchmark of the study, after SWE-bench Verified: 89 open-ended terminal
tasks, claude-code on aws/claude-sonnet-5, run live through the harness. Four
arms, same as SWE: baseline (off passthrough), context-guru (codesmart),
headroom (hd-cache), rtk. The claude-code trajectory parser, the cache-aware
cost model and the summarizer are agent-specific, not benchmark-specific, so
every number is computed identically to the SWE arms.

Harnesses: terminalbench.py / _headroom.py / _rtk.py (thin adaptations of the
SWE ones, dataset + jobs-root differ) and gen_tb_docs.py for the per-arm pages.

What the run shows: the agent is ~98% cached here too, so cache-read is again
the largest cost term — but cache-write, a rounding error on SWE-bench, becomes
the deciding term on TB's ~1.7M-token contexts. Six baseline trials are
degenerate (baseline aborted in 2-6 steps where the arms ran 50-160), which
inflates the apparent regression; over the 83 clean tasks context-guru is -9.7%
and headroom -16.0%, with only rtk regressing. That correction is stated up
front on the comparison page and the six tasks are queued for re-run.

improvement-plan.md carries the synthesis of both benchmarks: cost tracks agent
steps (r=0.95), one cache-write costs 11.5 cache-reads, unique token removal is
0.02-0.13% of the billed total, and cache_control placement is metadata rather
than hashed content — so moving a breakpoint is free.

Also fixes swebench.py: captures and dumps now live under the run's jobs-root
instead of a fixed /tmp path that start_proxy unlinks, which is how an earlier
472-request capture was truncated mid-analysis. Adds the cacheonly arm that
isolates the prompt-cache lever from token reduction.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
…ct the xdedup premise

Two corrections to the TB study, both from re-deriving the numbers from the
row files rather than trusting the per-arm totals.

1. Six baseline trials are degenerate: the baseline aborted in 2-6 steps while
   the compaction arms ran 50-160. mteb-leaderboard, polyglot-rust-c and
   extract-moves-from-video alone account for $11.5 of apparent regression.
   On the 83 clean tasks context-guru costs $90.34 vs baseline $100.17
   (-9.8% including its own haiku cost, -12.7% on model cost alone), solves
   +2, and takes 8.3% fewer steps. So TB does not invert the SWE result; the
   +1.7% headline was an artifact. headroom recomputes to about -16%; rtk
   remains a genuine regression.

   What IS different on TB survives the correction: cache-write, a rounding
   error on SWE-bench, is the deciding term on 1.7M-token contexts.

2. The cross-turn dedup premise is refuted. Measured on the raw captures
   (1,325 requests / 51 sessions), 232 of 232 re-sent large outputs live at
   exactly one stable message index, and 100% of consecutive turn pairs have
   the previous turn as a byte-identical prefix. The agent appends; it does
   not re-send. Those 5.46M tokens sit in the cached prefix and already bill
   at the cache-read rate, so an xdedup component would have no legal
   opportunity to act, and rewriting them would convert reads into writes at
   11.5x. Independently re-checked: 0 of 77 large outputs ever appeared at a
   second index.

Also documents the baseline's two-stage merge in REPRODUCE, because the
intermediate rows file sums to $71.44 and does not reproduce the published
$100.81 -- that gap is what surfaced both errors.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
… the bug was a latency tautology

The plan asserted that context_guru_expand is 'referenced 1,496x and callable
0x' because the tool is never registered on the streaming path, and made
registering it the single biggest reward lever. Both halves are wrong.

expand.Inject does fire on real streaming requests, and proxy.serve does
buffer and aggregate SSE when markers are present -- there is no streaming
short-circuit. More decisively, a live SWE run recorded bounces=1 with 3,372
tokens restored: RecordExpand has exactly one reachable call site, inside the
continuation loop, only after a model-issued expand call resolves against the
store, and all traffic was SSE. So restoration completed through the streaming
path.

The 4.8M figure was cumulative, re-counting each compaction every turn history
is re-sent. Unique is 234,119 tokens behind 103 markers on TB and 15,457 behind
29 on SWE -- 21x and 8x smaller. Demand is genuinely low, not blocked.

The real defect was a tautology: hasMarkers tested the raw body for the escaped
marker sequence, and the injected tool description itself contains it, so every
SSE response was buffered and the documented zero-added-latency fast path never
engaged. Fixed by scoping the check to messages + system; marker-free TTFB went
1007ms -> 43ms, and live buffering fell from an implied 100% to 27.3%.

Both this and the retracted C1 were premise errors from trusting the
change-log dumps -- which only record messages a component already acted on --
over the raw request captures. That lesson is now recorded in the section.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
…ng premises taught

An aggregate moving in the predicted direction is not evidence the predicted
mechanism operated. Four premises in this plan were wrong, and each failed the
same way: a derived artifact was trusted over the raw request stream.

- C1 xdedup: a 39.8x re-send factor was read as tokens re-sent as new bytes.
  232 of 232 large outputs sit at one stable message index; the agent appends,
  so those tokens are cached-prefix reads and the component could never have
  acted.
- B2 expand: 'never registered on the streaming path' was false; a live agent
  restored 3,372 tokens through it. The 4.8M was a cumulative re-count against
  a 234k unique figure.
- prefixpin: early-index churn measured 0 in ~6,500 comparisons on claude-code.
  An earlier 52% reading was concurrent sessions sharing a byte-identical first
  message and being diffed against each other.
- async cache-write: -45%/-39% was read as the tail-protection working, but the
  protection only stripped context-guru's own breakpoints and never the agent's,
  so lower cache-write came from writing fewer breakpoints instead.

Three of the four produced a number pointing the right way for the wrong
reason, which is why they survived review. Records the five countermeasures,
the most useful being: group lineages by append-only prefix match rather than a
first-message hash, and instrument 'did the component act' separately from 'did
the metric improve'.

Also revises F2: cacheinject is not a dead component. It read as inert partly
because its breakpoints were discarded by the writeback layer before reaching
the wire (46 applied, 0 forwarded). Once forwarded, placement measures mildly
harmful, so the open question is whether it belongs in the default preset.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
…e instances, two of them mine

Four more premises fell the same way since F-1 was written, taking the count to
eight:

- cacheinject read as 'provably inert' when it was in fact applying 46
  breakpoints and forwarding 0 -- the writeback layer discarded every one. Two
  benchmark studies concluded things about breakpoint placement while measuring
  a component whose output never left the process.
- the follow-on claim that placement is HARMFUL (+61.9% cache-write/step) does
  not survive either: 0 of 106 marks land above the agent's own breakpoint, so
  the proposed mechanism is ruled out, and the arm's acted=0 is a tautology of
  its design rather than proof the delta was placement.
- cachesplit cannot fire on Terminal-Bench at all. TB runs the Agent SDK, which
  never appends the git/env snapshot the CLI does: all 73 captured requests
  carry 3 system blocks and zero volatile-tail markers. Zero legal opportunity,
  the same shape as the refuted xdedup premise.
- the same split is a silent no-op on Bedrock Converse, where cachePoint is its
  own array entry after the block, so the volatile half is inserted before it
  and the breakpoint still covers the churn -- while reporting Changed: true.

Two of these were mine as orchestrator, and one was an UNFAVOURABLE number I
accepted without checking its mechanism. That is the more useful half of the
lesson: the bias is not optimism, it is incuriosity, and skepticism applied only
to good news is not skepticism.

Adds four countermeasures: a component reporting that it acted is not evidence
it acted usefully; check the favourable metric had the opportunity to be caused
by your change; verify the verifier (two 'defects' here were bugs in the
checking script); and a sum over heterogeneous tasks can be one task -- an
interim TB delta read -40.2% with a single trial carrying half of it, so report
the median per-task ratio and a leave-one-out beside any aggregate.

Rewrites F2's cacheinject entry as the full three-stage arc, since it is the
clearest worked example of the rule in the document.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Re-measures context-guru on TB after the 15 cache/filter/observe PRs landed on
main, as a fifth arm alongside the original four. The original study is left
unchanged below it.

Config is cgfinal = [format, dedup, cmdfilter, extract, cachesplit], chosen on
per-component evidence rather than maximal token reduction: extract_llm is 82x
underwater once its saved tokens are priced at the cache-read rate they actually
bill at, failed_run acted 0 times while burning 28.8 s, and cacheinject was
removed from every preset by #36.

Result on 81 clean tasks: 61 solved vs baseline 53, total $79.32 vs $94.85, own
LLM cost $0 vs the previous arm's $2.97, added latency 38.5 ms vs 449.8 ms.

Two framing decisions the numbers force:

The -16.4% aggregate is single-task sensitive -- path-tracing alone accounts for
most of it, and an independent re-derivation with a stricter degenerate rule gave
-13.7% dropping to -2.8% on the same exclusion. The median per-task ratio, -7.8%
with 49/81 cheaper, is the figure to quote for a normal task. Both are published
because they differ by 9 points.

The one result needing no caveat is cache-write/cache-read returning to 1.86%,
identical to baseline, where the previous arm ran 2.86%. That is the cache-write
tax this study named as the deciding term on TB, and being a ratio rather than a
sum it holds under every exclusion rule tried.

Records what could NOT be verified: #40's freeze-TTL work has all five frozen_*
counters at zero because its only callers are the three components this config
excludes, so the arm is not evidence for or against it and none of the cost
improvement may be credited to it. cachesplit likewise has zero legal opportunity
on TB, because the Agent SDK never appends the git snapshot the CLI does.

Regressions published rather than omitted: system-administration is +17.2% cost
AND -2 solved, security +25.6%, fresh_input 3.8x baseline, and small tasks still
inflate up to +311% at n=1 -- size-gating remains an unclaimed win. Also states
plainly that cgfinal's raw model cost nearly ties the old arm and its cache-read
is higher, so it wins mainly by not spending $2.97 on haiku.

Limitations: headroom and rtk cannot be re-derived because their trial artifacts
are pruned from disk, so those columns are cited rather than recomputed; single
trial per task; one task still running at report time.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants