CLAUDE.md@.claude/keepalive · git:20261013.b296641 · 2026-10-13 · sha256 a20e097d6b1d4b97

CLAUDE.md@.claude/keepalive git:20261013.b296641A

Immutable. This exact content never changes and is served at /api/v1/blob/a20e097d6b1d4b97.

# 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`).

- **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
  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.
- **`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.
- **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.
- **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.