SLOPSHOPPER

effort-probe

Measures whether a turn.step effort rewrite preserves the prompt cache: switches one middle step and logs each step's usage

new
★ 1v0.1.0MITupdated 2026-10-09alienfast/claude/mods/effort-probe
A shopper browsing a rack in a slop shop
README

effort-probe

Measures whether a turn.step effort rewrite preserves the prompt cache. One scripted claude -p conversation of four main-loop requests (three chained Reads, then the answer) runs with this mod loaded; in switch mode the hook rewrites request 1's effort to low, in control mode it rewrites nothing. Each request's usage lands in a JSONL ledger.

mods/effort-probe/run.sh control          # baseline
mods/effort-probe/run.sh switch           # request 1 at low effort
mods/effort-probe/run.sh switch sonnet    # another model

Result, 2026-10-09

Claude Code 2.1.295, opus[1m] answered by claude-opus-5-5, --effort high.

requesteffort sent (control / switch)cache_read controlcache_read switchcache_creation controlcache_creation switch
0high / high21,97723,76037,65635,878
1high / low59,63359,6383,6443,644
2high / high63,27763,282148148
3high / high63,42563,430143143

The switched request and the switch-back request read the whole prefix from cache and wrote only the new turn, the same as the control, so a turn.step effort rewrite is cache-safe on Opus 5.5 on this build. Request 0 differs only because the two first requests ran concurrently and each wrote its own entry. The debug log records request ids, not bodies, so the wire shape (per-message effort system message or top-level field) is not observed directly.

Re-run after a Claude Code upgrade before trusting effort-phase with per-step switching; its max_switches_per_turn option is the fallback if the numbers change.

Checks

claude plugin validate mods/effort-probe
claude plugin test mods/effort-probe
mods/typecheck.sh mods/effort-probe
Source 1 files
hooks/register.ts 47 lines
1import type { Register } from 'claude-code'
2
3// One row per main-loop model request. `effortIn` is what the engine was about to send; `effortOut` is what this
4// hook sent. The usage counts are what the API reported for that request, so a prefix rewrite shows up as
5// cache_read collapsing and cache_creation jumping on the switched step relative to its neighbours.
6type Row = {
7  index: number
8  effortIn: unknown
9  effortOut: unknown
10  model: string
11  answeredBy: string | null
12  usage: unknown
13  toolUses: string[]
14}
15
16const rows: Row[] = []
17let mode = 'switch'
18let outPath = ''
19
20export const register: Register = (on) => {
21  on('session.start', async ($, e, next) => {
22    mode = (await $.env.get('EFFORT_PROBE_MODE')) ?? 'switch'
23    const out = await $.env.get('EFFORT_PROBE_OUT')
24    const id = await $.session.id()
25    outPath = out ?? `${await $.session.cwd()}/effort-probe-${mode}-${id}.jsonl`
26    return next(e)
27  })
28
29  on('turn.step', async function* ($, e, next) {
30    if (e.agentId !== undefined) return yield* next(e)
31    const switched = mode === 'switch' && e.index === 1
32    const effortOut = switched ? 'low' : e.effort
33    const result = yield* next(switched ? { ...e, effort: 'low' } : e)
34    rows.push({
35      index: e.index,
36      effortIn: e.effort,
37      effortOut,
38      model: e.model,
39      answeredBy: result.usage?.model ?? null,
40      usage: result.usage,
41      toolUses: result.toolUses.map((t) => t.name),
42    })
43    await $.fs.write(outPath, rows.map((r) => JSON.stringify(r)).join('\n') + '\n')
44    return result
45  })
46}
47