CLAUDE.md@.claude/keepalive · diff
git:20261013.b296641 to git:20261014.0f16275
17 added, 16 removed. Audit A to A.
# The cache keepalive
`hooks/keepalive.py` wakes an idle session shortly before its one-hour prompt
cache expires, because the next prompt after expiry re-caches the whole
- conversation at 20 times what a cache read costs — 40 on a model whose read
- rate is below the usual tenth of input (`.claude/costs/prices.json`).
+ conversation at 20 times what a cache read costs — 40 on a model whose read rate
+ is below the usual tenth of input (`.claude/costs/prices.json`).
- - **The watcher is a background Bash task, the one exception to CLAUDE.md's
- ban on `run_in_background`.** The platform reclaims an idle container within
+ - **The watcher is a background Bash task, the one exception to CLAUDE.md's ban
+ on `run_in_background`.** The platform reclaims an idle container within
minutes unless a background task is running; an async hook does not count.
`keepalive.py watch` polls the transcript and exits
`MUTHUR_CACHE_KEEPALIVE_LEAD` seconds (default 300) before the cache expires,
and the task's completion notice is the wake.
- **It runs only while losing the session would lose something.** The
`UserPromptSubmit` hook asks the agent to start it as a turn's last action,
unless the branch alone lets a fresh session continue — the work at a loop
boundary, nothing said since that the repo lacks — and to `TaskStop` a running
- one once that holds. Only the agent knows what the conversation holds that
- the branch does not. `TaskStop` ends a task without a completion notice, so
+ one once that holds. Only the agent knows what the conversation holds that the
+ branch does not. `TaskStop` ends a task without a completion notice, so
stopping one wakes nothing.
- **Every wake is one visible line.** The harness rejects a turn with no visible
output and forces a second request, so the instruction asks for one line
- rather than silence — and spends it on the two answers that end the spell,
- in plain words an operator types without knowing the loop: one parks the
- session (`/relay park`), one just stops. The hook maps both back.
+ rather than silence — and spends it on the two answers that end the spell, in
+ plain words an operator types without knowing the loop: one parks the session
+ (`/relay park`), one just stops. The hook maps both back.
- **`MUTHUR_CACHE_KEEPALIVE_RENEWALS` (default 0) renewals per idle spell**,
- which an operator prompt resets. The wake after the last one runs `/relay
- park` (`@.claude/skills/relay/SKILL.md`), and no watcher follows it — so by
- default the first wake parks. A renewal competes with that summary, which
- makes picking the work up cheap.
+ which an operator prompt resets. The wake after the last one runs
+ `/relay park` (`@.claude/skills/relay/SKILL.md`), and no watcher follows it —
+ so by default the first wake parks. A renewal competes with that summary,
+ which makes picking the work up cheap.
- **More than 5 renewals is not recommended.** A renewal is two requests (the
Bash call starting the next watcher, then the line), each reading the whole
cached conversation, so it costs 2 ÷ 20 = 10% of a cold re-cache, or 5% where
- the ratio is 40. Renewals the operator never comes back for are spent outright:
- 5 come to 25–50% of a cold re-cache, and 10–20 to all of it.
+ the ratio is 40. Renewals the operator never comes back for are spent
+ outright: 5 come to 25–50% of a cold re-cache, and 10–20 to all of it.
- **Only a one-hour cache is kept**, read off the session's own cache writes by
`.claude/costs/lib/restart.py`. A five-minute cache would take a wake every
few minutes.
- **`tmp/keepalive/period`** holds a number of seconds that replaces the
computed deadline, for a live check in minutes rather than an hour. Settings'
`env` is read at session start, which is why this knob is a file.
- `MUTHUR_CACHE_KEEPALIVE=off` in `.claude/settings.local.json`'s `env` disables it.
+ `MUTHUR_CACHE_KEEPALIVE=off` in `.claude/settings.local.json`'s `env` disables
+ it.