Whetstone

strain

✅ Open source (MIT) · ✅ Works as a plain prompt in any agent · ✅ Fires on its own where hooks exist

Tells you when a session has gone bad — before its answers do.

A long agent session degrades quietly. The context window fills up, the same bug comes back a third time, an answer from an hour ago turns out to be wrong, and the work carries on regardless, because nothing in the loop is watching the loop. strain watches: it counts how loaded this one conversation has become and makes the agent report it, on a schedule it cannot quietly skip.

The failure it exists to prevent is not a crash. It is silence — an agent that never mentions the session has gone bad, and lets you find out from the output.

Quick start

  1. Install the plugin.
  2. Work as usual. Every 10 tool calls the agent is asked to run the check and report a tier — 🟢 Healthy, 🟡 Mid / High, 🔴 Warning / Danger.
  3. When it says wrap, wrap — and stamp it:
    bash <strain>/scripts/strain-wrap.sh --label "what was handed off"
    

    <strain> is the plugin folder; every tick prints the commands with the real path and this session’s key already filled in (a record without the key is refused — see Which session a record lands in). A stamp records that the session wrapped; it resets nothing — a new session starts at zero anyway.

Nothing to configure. Nothing leaves your machine.

What a readout looks like

🟡 High — context 136k/200k (68%), of which 70k was the boot itself.
1 main goal, 3 side tasks. Suggest finishing the current thread and wrapping.

That number on the first line is measured, not estimated — see below.

Starting and wrapping a session (0.8.3)

Strain links sessions only when they are signed, and before 0.8.3 nothing made signing happen at the start: the start line carried calibration only, the sign step first appeared at the 10th tool call, and on one host the skill’s own description (“not for short one-pass tasks”) talked the agent out of loading it. Now:

  1. The start line carries the steps. While a session is unsigned, strain’s start line (what the agent reads before its first reply) lists numbered steps with the sign command filled in. The strain:start skill holds the same steps, under a name that matches the moment.
  2. One command, a stable name. strain-sign.sh with no --agent signs as <host> · <project folder> — for example Claude Code · my-app — the same every session, so the next session finds this one. A name you give your agent wins. Where the folder names nothing (local Cowork), the host name alone.
  3. You are asked properly. If your host asks you to approve the command, the agent describes it in your language, in plain words, and keeps the prompt open until you answer. Say no, and the agent does not sign and does not ask again before the wrap; the hooks still count.
  4. Once: the instruction file. The agent offers to add a short block to your instruction file, so every new session signs in by itself — the global file first (~/.claude/CLAUDE.md on Claude Code, ~/.codex/AGENTS.md on Codex: every project, this computer only), or the project’s own file. It shows you the block (strain-sign.sh --instructions) and adds it only if you agree. Say no and strain never asks again on that host (strain-sign.sh --decline-instructions).
  5. At the wrap, the agent checks permission again, then signs if needed and stamps the wrap. Still refused: no stamp, just the counts.

What strain cannot control:

Sub-agents and unattended runs (0.8.4)

Strain measures one conversation with a person in it. Two kinds of run are not that, and strain now leaves them alone:

  1. A sub-agent — a helper your agent starts inside its own session. Claude Code sends its tool calls with the parent’s session id and the sub-agent’s own agent_id; strain skips every call that carries one, so the helper is neither counted into the parent nor handed the strain check. On a host that does not mark sub-agent calls, tell the helper to ignore any strain message it sees (the strain:start skill says so).
  2. An unattended run — a headless claude -p, a scheduled trigger, a CI action. Nobody can approve a sign-in there, so the start line says “unattended run” instead of listing the sign-in steps: do not sign in, do not wait, carry on. Strain still counts the run. Set STRAIN_UNATTENDED=1 to mark any run unattended, or =0 to keep the steps.

Two modes, and it always says which

Mode When What you get
measured the host publishes a per-session transcript with token usage (Claude Code does) real context numbers: how full the window is, and what the boot alone cost before any work happened
estimated a transcript exists but carries no token usage fill estimated from transcript bytes (~4 bytes/token), labelled ESTIMATED in every readout
inferred no transcript at all behaviour counting: tool calls, compactions, and what the task list shows

Since 0.8.0 the engine asks host adapters first — one small file per host that keeps its log in its own shape. Codex is the first (see Codex below); a host with an adapter is read in measured mode from its own numbers.

inferred is not a failure — it is the original design and it works. What would be a failure is printing a confident number nobody measured, so the mode travels with every readout.

The baseline is worth its own mention: the tokens your session spent before doing anything at all — system prompt, tool schemas, project instructions, skills. It is the floor the session can never get back under, and on a loaded setup it is routinely a third of the window.

Measured mode also observes which model is running — from the same transcript line that carries the usage, so a mid-session model switch is seen too. Two things depend on it: the model log (one appended row per observed change, never per tick), and the denominator. Every readout names where its window came from (0.8.0), in this order of trust: env override (STRAIN_CONTEXT_LIMIT) · host-reported (the host states its window live — Codex does) · calibrated: <product> · <model> (a record you made, see below) · model hint (inferred from the model id: fable, opus-5 and [1m] variants → 1M, and so on) · default (200k). So the bracket reads, for example, window 1M (model hint) or window 258k (host-reported · Codex). This matters more than it sounds: a hardcoded 200k denominator once reported a 1M-window session at 69% when it was actually 14% full — a wrong number delivered with full confidence, which is exactly what this tool exists not to do.

When strain cannot read your host (0.6.0, reworked in 0.8.0)

The first cross-model field report made a gap plain: a session on a non-Claude host had its numbers on screen — the host reported a 258,400-token runtime window and per-turn usage — and strain still ran blind, because the transcript reader knew one host’s shape. “The host exposes nothing” and “the host exposes something strain cannot parse” used to land in the same coarse mode. Two front doors fix that, and an adapter (0.8.0) closes it for good for hosts that keep a durable log.

When nothing is measured, strain says which kind of nothing (0.8.0). The tick never says Healthy for a reading it does not have; it proposes UNMEASURED and names one of three kinds, each with its own next step:

Kind What happened Next step the tick gives
no source no log strain can read on this host look where hosts keep usage (a session log, a status or usage command, a usage API, the settings pane), then end with ONE record: a feed, or --no-source --checked "<places looked>". The tick repeats until one exists, so skipping it is visible.
unusable a source was read and gave no usable number (no usage lines, a window of 0, a log that names another session, or a fill at or past 100%) read the host’s own number and feed it with its provenance; a fill past 100% is strain’s arithmetic going wrong, so the tick says the used count is a lower bound, lists what to check (model · effective window · double counting · compaction boundary) and offers a hand-sent bug report
stale a compaction or a model switch happened after the last reading a host that measures itself re-measures at its next request (nothing to do); a fed number needs a new feed

UNMEASURED can also be recorded — strain-level.sh UNMEASURED — so an agent with no number to give never has to invent a tier.

Denominator — record a window once, for one product and one model, sourced and typed:

bash <strain>/scripts/strain-calibrate.sh --product "Example Host" --model ex-model-1 \
     --window 258400 --basis runtime --source "host runtime log"

A record belongs to ONE <product> · <model> (0.8.0). Until 0.7.0 there was one record per machine and every session on the machine divided by it: one host’s 258,400-token window became the window of every session on another host next to it, and a session 19% full was told to wrap now. Now the model must match always; on a host strain recognises (Claude Code, Cowork, cloud Cowork, Codex) the product must match too; on a host it does not recognise, the model alone picks — among records made for unrecognised hosts, and only when exactly one fits. A window the host reports live beats any record. The old calibration.json is no longer written; strain reads it as one more record, and one that names no model is never applied (the tick says so once). strain-calibrate.sh --show lists every record and says which one rules this session, and why.

--basis is required and keeps the record honest: nominal is the published capacity, runtime is what the host reports live (often smaller — e.g. a post-compaction window). A runtime reading mistaken for the model’s nominal size corrupts every percentage after it. A record expires (default 30 days) and a stale one falls back loudly — it never silently keeps ruling.

Numerator — when the host shows its usage, hand it over at recording time:

bash <strain>/scripts/strain-level.sh Mid --ctx-used 65749 --ctx-source "host status command" \
     --ctx-provenance host-reported

The readout then says fill 25.4% — agent-fed: 65,749 of 258,400 tokens (window: …). agent-fed is its own mode, printed every time: a fed number is honest input with a named source — never dressed up as a measurement strain made itself. --ctx-provenance says whether you read it from the host (host-reported) or estimated it yourself (agent-estimated).

A host whose log strain cannot read yet? If the number lives in a durable log, write an adapter proposal — where the log is, which fields carry the usage, one redacted sample line — with strain-report.sh --note "…", and send it to the maintainers. Do not patch the installed plugin: the next update overwrites a local patch, and it helps nobody else.

No hooks on your host? Then ticks will not fire on their own. Adopt the manual cadence the skill describes: run the strain check by hand every ~10 tool calls or at every milestone, and feed the reading if your host shows one. Name your session once with --session <name> on every record command — strain does not guess one for you. An instrument nobody polls is an instrument that stays quiet — on hook-less hosts the polling is yours.

Codex (0.8.0)

Codex writes one log per session — ~/.codex/sessions/YYYY/MM/DD/rollout-…-<session id>.jsonl (or under $CODEX_HOME) — and records, on every request, how many tokens went in and how big the window is. strain reads that receipt directly: the newest last_token_usage.input_tokens (the cached part is already inside it, so nothing is counted twice) over the host-reported model_context_window. Nothing to configure, no calibration record needed. Details:

One session, one reading

Strain measures a single conversation, keyed by its session id. Two agents on the same project in two windows are two sessions, and their counts never add up:

~/.local/state/strain/sessions/<session-id>.json

This is not a detail. An earlier internal build kept one global state file with a single session slot, so a second session simply overwrote the first and both counters became meaningless. The session id is also how the host names the transcript — so whose strain is this and can I read this session’s real context size are the same question.

Which session a record lands in (0.8.0)

The hooks always know their session — the host puts the id in every payload. The agent’s shell does not, and until 0.7.0 a record command without a key fell back to “the session last seen in this folder”, then “the most recent session on the machine”. With two agents working at once, a tier, an escaped error or a wrap could land in whichever session fired a hook last, while the right session’s number never moved.

Now a record (strain-level.sh, strain-signal.sh, strain-sign.sh, strain-wrap.sh, strain-report.sh) needs a key, from one of these, in order of trust:

  1. --session <id>, typed.
  2. STRAIN_SESSION — every tick prints it into the ready-made command; on Claude Code the SessionStart hook also exports it into the agent’s shell (through the host’s CLAUDE_ENV_FILE hand-over), so plain commands land in the right session.
  3. The host’s own shell variable — Codex sets CODEX_THREAD_ID.

Claude Code shells also carry CLAUDE_CODE_SESSION_ID; it is not documented, so strain uses it only as a cross-check: when sources disagree, the record is refused and both are named. With no key at all, the record is refused (exit 2) with up to three candidate sessions and a ready command for each — it never picks one. Reads (--get, --show, --list, --status, --receipt) may still guess, and say “guessed” when they do.

What you see, and what your agent sees. The first sign of a session tells you one plain sentence, and strain-sign.sh --receipt repeats it any time:

Strain is on. This conversation is 10% full — strain will tell you when it's time to wrap up and start fresh.

Before the agent’s first reply no model has been seen yet, so the size is an estimate over a default window, and the sentence says so (0.11.1):

Strain is on. A first estimate puts this conversation at about 25% full; the number can change once strain reads the real size. Strain will tell you when it's time to wrap up and start fresh.

The agent gets the details on the side — host, session, where the reading comes from, where the window came from, whether the hooks are saving, whether its own shell can write (0.8.1; 0.8.0 showed users the agent’s version, which was correct and unreadable):

Strain receipt — host Claude Code · session 0a8e0001 · signed as Claude Code · my-app · reading Claude transcript at 09-30 18:15 · measured · window 1M (model hint) · hooks saving (last 09-30 18:15) · this shell can write · no ledger

On Claude Code the hook shows you the sentence itself, once per session (the host’s systemMessage). In the desktop app it sits folded into that step’s title as “received a notice” — open the step to read it. Two other plain sentences use the same channel, because they must not depend on the agent passing them on: when strain can’t tell how full the conversation is, and when it can’t save on this computer. STRAIN_USER_MESSAGES=off turns all three off. On other hosts, the skill asks the agent to relay the sentence as it is.

Where strain writes

Everything strain keeps is in one folder outside your project: ~/.local/state/strain (or $XDG_STATE_HOME/strain, or wherever STRAIN_STATE_DIR points). Inside it, every file is either one session’s own (sessions/<id>.json, a draft under reports/) or keyed per entry — by session (index.json, model-log.jsonl), by agent (ledgers.json), by product · model (models.json) or by host (prefs.json, 0.8.3: an instruction file the user declined). A selftest check walks a full scenario and fails if a file appears that no reader could attribute.

Cloud Cowork (0.7.0)

Tip — let your agent carry it over. Put a handoff file (for example HANDOFF.md) in a folder connected to the task, and add these two rules to your agent’s instructions:

  1. “When you wrap up, run strain-level.sh --show and write one line into HANDOFF.md: the date, tool calls, compactions, errors that reached me, and the final strain level.”
  2. “At the start of every conversation, read that line in HANDOFF.md and tell me how the last session went.”

Keep the desktop app open while the agent writes the file, and check that it arrived — we have seen a cloud write land an older copy.

This release has not been tried in a cloud task yet. The cloud behaviour above comes from testing an earlier build, and the tip has not been tested end to end.

Signed wraps and the project ledger

The path cannot tell agents apart (two windows on one project look identical to the hooks), so identity is declared, not derived:

bash <strain>/scripts/strain-sign.sh --agent ana --ledger ./Log.strain

Signing names this session’s agent. With a ledger, the first sign of a session also prints a previous-session report (0.7.0): the newest earlier session of the same agent in that book — its tool calls, compactions, errors escaped and caught, whether it wrapped (and when), and whether it kept working after the wrap. It only reads; nothing is carried over.

Until 0.6.0 a wrap was a shared marker that reset counters at the next session start — which reset another live session of the same signature, and reset a session that kept working in the same full context after its own wrap. Since 0.7.0 a wrap resets nothing: it records wrappedAt / wrapTick in the session’s own state and books one wrap row. An old wrap-marker.json in your state dir is ignored and can be deleted.

The --ledger part is optional and adds a durable account book: an append-only JSONL file (suggest Log.strain at the project root, gitignored) that receives one boot-sign row when a session signs and one wrap row (verdict, label, tier, tick) when it wraps. It records the project’s chain of sessions — who worked, when, wrapped how — and it never inherits counters across sessions: one session, one reading, unchanged. Appends are flock-guarded, so concurrent signers cannot tear a row. The path needs saying only once (0.5.1): it is remembered per agent (ledgers.json in the state dir), so later sessions sign with --agent alone.

Session numbers (0.9.0)

With a ledger, the sign gives the session its number and says it in the receipt — Strain · Owner: ana · S12 — signed · booked — and writes it into the boot-sign row ("n": 12). Like a CI run number it is given at the start and kept on resume: the same session signing again gets the same number. Like a database sequence, a number once given is never reused: a crash may leave a gap, never a duplicate. The book is read, the number picked and the row appended under one lock on the book, so two sessions signing in the same second cannot both take the same number. (Numbering at the end of a session, from finished reports, cannot see a session still running — two chats got one number that way.)

Other names (0.10.0)

People call an agent by more than one name — Orion, Ori, 小猎. Without help, a sign under a nickname starts a separate chain: no book, no number, no previous-session report. Declare the other names once, with the agent’s one name:

bash <strain>/scripts/strain-sign.sh --agent Orion --aka Ori --aka 小猎

They are kept in the registry (ledgers.json, aka) and once in the book (a names row), and every later sign resolves them: strain-sign.sh --agent 小猎 signs as Orion — the same book, the next number, the same previous-session report — and the receipt shows both names: Strain · Owner: Orion (signed as 小猎) · S3 — signed · booked. The boot-sign row carries the agent’s name in agent and the typed one in as. Names match ignoring case. A name that is another agent’s own name, or already another agent’s other name, is refused with the reason and not kept. A name two agents claim (hand-edited files) signs as typed, with a warning — strain never guesses. A wrap under another name books the agent’s one name. No command removes another name yet: edit ledgers.json and the book.

A wrap’s start (0.10.0)

A wrap takes time. strain-wrap.sh --start marks its first step: wrapStartedAt (and wrapStartTick) in the session’s state and a wrap-start row in the book, so a board can show “wrapping” from that moment instead of guessing from the fill. strain-wrap.sh --cancel calls a started wrap off (the user says go on): it removes the start and books a wrap-cancel row; with no start it writes nothing and exits 2. The stamp itself (strain-wrap.sh) is unchanged and keeps the start as history; --status shows both.

Signing from a bare terminal

The sign does not have to come from inside the agent’s own shell. Some sessions cannot reach the state home from where they run — a sandboxed agent, a remote shell — and there the honest move is delivery by hand: the agent hands over a filled-in command, and the person at the machine runs it in any plain terminal:

STRAIN_SESSION=<session-id> bash <plugin>/scripts/strain-sign.sh --agent ana --ledger /path/to/project/Log.strain

<plugin> is wherever the installed copy lives (for Claude Code, ~/.claude/plugins/cache/whetstone/strain/<version>). The key is required (0.8.0): without it the sign is refused and lists the candidate sessions with a ready command for each — it no longer picks “the session this folder saw last”. The stdout lines — Strain · Owner: ana — signed (…) and the plain Strain is on … sentence — are the proof: paste them back to the agent and the session is signed. Until then the session simply stays unsigned: measured, but part of no chain.

Your own lines at fill levels (0.11.0)

strain’s tiers and directives are its own fixed text. To have strain say your words when a session reaches a fill level, write them in a lines file:

{"strain_lines": 1,
 "lines": [{"at": 50, "say": "save a checkpoint", "mark": "backup"},
           {"at": 70, "say": "begin the hand-off", "mark": "wrap-start"}]}

A backup mark (0.11.0)

strain-wrap.sh --backup records that a mid-session backup was taken: a {ts, tick, pct} entry in the session’s backups (with --label, its name) and, for a signed session, a backup row in the book. It is not a wrap and not a signal — signals are errors, and a repeated kind earns a pattern note. With --start, --cancel or --with-debt it is refused and writes nothing. --status shows the count, the last backup, the lines passed and the peak fill.

What it counts

Context occupancy, when measurable. Plus behaviour, always:

Hard signals are absolute (capacity never dilutes accountability), but only the ones that escaped — reached the user or shipped work — move the tier. An error caught and fixed before delivery is a working immune system, not exhaustion: it is recorded, and a repeat of the same class earns a pattern note, without driving the tier. Since 0.8.5 each signal also records the fill it was made at, and only escaped signals made past 50% fill move the tier — earlier ones are kept for the record (Line B, below).

Since v0.3 the tier is a two-line model — capacity and conduct scored separately, and the higher one wins:

The final tier is the max of the two lines. (v0.4 to 0.8.4 also had a combination alarm: Line A at Warning/Danger and 3 escaped → Danger. 0.8.5 removes it — the owner’s ruling puts that compound state inside Line B, at five escapes made past 50% with the fill at 70%, and a second, looser path to Danger would contradict it.) Errors inside the throttle zone (≥80%) are still annotated as likely capacity-induced, never auto-escalated: the two lines answer different questions. The proposal is still allowed to DECAY when the load does, and nothing escalates on tick count — the old “continuing past a Warning ⇒ Danger” rule pinned Danger at a measured 33% fill, three sessions running, and stays deleted.

A compaction cuts the context; it is not a load reading. Until 0.6.0 it floored the tier (1 → High, 2+ → Warning), which read a session that had just been compacted to a small context as heavily loaded. The fill after it is the measure.

Commands

All of these live in the plugin folder: bash <strain>/scripts/<name>. The tick prints each one ready to run — this session’s key, the state folder and the plugin’s absolute path filled in — so the agent copies it from there. (Until 0.6.0 the tick printed "$CLAUDE_PLUGIN_ROOT/scripts/…"; that variable exists only inside the hook process, so the command failed in the agent’s own shell.)

Command What it does
strain-level.sh <tier> record the tier for this session (UNMEASURED is recordable too)
strain-level.sh <tier> --ctx-used <n> --ctx-source "<where>" [--ctx-provenance host-reported\|agent-estimated] record a tier AND feed the host’s own usage reading (agent-fed fill, source named)
strain-level.sh UNMEASURED --no-source --checked "<places>" record that this host shows no usage anywhere you looked — ends the tick’s “no source” checklist
strain-calibrate.sh --product <p> --model <m> --window <n> --basis nominal\|runtime --source "<where>" record the window of one product · model — sourced, typed, expiring
strain-calibrate.sh --show list every record, whether each is valid, and which one rules this session
strain-report.sh [--note "…"] [--title "…"] draft a bug report (or an adapter proposal) about strain for the user to send by hand; nothing is sent
strain-level.sh --get print the current tier
strain-level.sh --show the whole state, including the context reading and which file it came from
strain-signal.sh <kind> --caught\|--escaped record a hard signal; escaped ones floor the tier, caught ones feed the pattern note
strain-signal.sh --list print this session’s signal ledger
strain-wrap.sh --label "…" [--with-debt] stamp this session wrapped / handed off; resets nothing; books a wrap row when signed with a ledger
strain-wrap.sh --status show this session’s wrap state (wrappedAt, wrapStartedAt)
strain-wrap.sh --start mark the wrap’s first step: wrapStartedAt, a wrap-start row (0.10.0)
strain-wrap.sh --cancel call a started wrap off: the start is removed, a wrap-cancel row (0.10.0)
strain-wrap.sh --backup [--label "…"] mark a mid-session backup: a backups entry, a backup row; not a wrap (0.11.0)
strain-sign.sh --agent <name> --aka <other> [--aka …] declare other names for the agent; later signs under them resolve to it (0.10.0)
strain-sign.sh [--agent <name>] [--ledger <path>] declare who works this session (with no --agent: <host> · <project folder>); with a ledger, book boot-sign/wrap rows and report the previous session of the same agent; the first sign prints the receipt
strain-sign.sh --receipt repeat the user’s sentence (stdout) and the agent’s details (stderr), including “signed as " or "not signed"
strain-sign.sh --instructions print the block for the agent’s instruction file, and say where this host’s files are and whether the block is in them
strain-sign.sh --decline-instructions the user said no to the instruction file: never offer it again on this host

Every record command needs this session’s key (see Which session a record lands in); the printed commands carry it.

strain-level.sh prints which session it resolved and where it wrote. That is deliberate: in an earlier build the writer defaulted to a different file from the one the hooks read, so recording a tier changed nothing anyone could see — and from the outside that looks exactly like a check that is working fine and always says Healthy.

Settings

Variable Meaning Default
STRAIN_N tool calls between ticks 10
STRAIN_STATE_DIR where state lives ~/.local/state/strain (or $XDG_STATE_HOME/strain)
STRAIN_CONTEXT_LIMIT context window size, tokens; overrides every other source host-reported > calibrated record > model hint > 200k
STRAIN_CAP_MID / _HIGH / _WARN fill % that enters Mid / High / Warning 50 / 60 / 70
STRAIN_THROTTLE_ONSET fill % where your platform’s model visibly degrades 80
STRAIN_WRAP_BUDGET context cost of a full session wrap, in fill % 6
(derived) Danger cap THROTTLE_ONSET − WRAP_BUDGET — never set directly 74
STRAIN_SUBSTRATE name the shell explicitly for the calibration line detected from the transcript path
STRAIN_DRIFT_GLANCE off drops the tick’s goal-drift glance (a non-flooring ask to check your task track for a side task outgrowing the marked MAIN goal — pairs with throughline’s convention; drift is a note, never a tier input) on
STRAIN_NO_MODEL_LOG stop recording which model ran which session unset
STRAIN_SESSION this session’s key for record commands printed in every tick; exported into the shell on Claude Code; records refuse without a key
STRAIN_USER_MESSAGES off stops strain showing its plain sentences to the user directly (Claude Code) on
CODEX_HOME where Codex keeps its logs, if not ~/.codex ~/.codex

Three numbers are yours to fill in — the defaults are honest starting points, not facts about your setup:

  1. The window (STRAIN_CONTEXT_LIMIT, or better: verify the inferred one). The denominator of every percentage. Get it from your platform’s official numbers, not from memory — a wrong window makes every reading confidently wrong.
  2. The throttle onset. 80 is an observed value on one platform (2026-09): the point where replies get shorter, reasoning goes quiet, narration stops. Watch for where it happens on yours, set it, and re-verify when the platform or model changes — it is a physical constant of your environment, not a preference.
  3. The wrap budget. 6 shipped as an estimate. The first field measurement on the design’s home environment came in at ≈9.5% (2026-09-11: wrap began at 49% fill, ended at 58.5%, including one failed-then-fixed check pass) — so treat 6 as optimistic. Measure your own: note the fill % right before and right after one real session wrap, and pin the difference. Until you do, treat the derived Danger cap as approximate — and err early, not late.

The Line-B ladder (3/4/≥5) is deliberately not an env knob — it is a team policy, and a policy adjustable by environment fiddling can drift silently. Change it by editing signal_floor in scripts/_strain_common.py, so the change is visible in review. The readout always prints the caps it actually used.

Design provenance. Model architecture: designed and ruled by Rae Sun (arcgram.io) — every load-bearing threshold (the throttle onset, the wrap budget, the Line-B ladder) is a maintainer ruling, not a model output. Drafted and adversarially hardened in multi-model AI collaboration; the acceptance rows in the selftest are the design’s contract.

Good to know

Companion

Strain says when to hand off. It does not do the handing off — that is Throughline, whose task track is also the cleanest source for the behavioural counts here. The two are built to be used together, and neither requires the other.

Verify it works

python3 tools/selftest.py -v

198 checks, host-independent: counting, per-session isolation, the tick firing on schedule and only then, the writer and reader agreeing on one location, wraps that reset nothing (and touch no other session), compactions stated but never floored, all three context modes plus a fed reading that is scored and kept, model observation (logged on change, a mid-session switch gets its own row, no empty rows), the denominator following the observed model, fill-band math on two capacities, escaped-vs-caught signal weighting, the boot line printing the same caps as the tick, a printed record command that runs from a plain shell, parallel ticks losing no count, the previous-session report, a calibration record ruling only its own product · model, the Codex reader (receipts, compaction, model switch, a log that names another session), records refusing without a session key, the three kinds of UNMEASURED and their next steps, the plain sentence for the user and the agent’s receipt, a failed save being said, every file strain writes being one session’s own or keyed per entry, and malformed payloads never failing a tool call — plus the negative fixture: the v1 bug (Danger pinned at a measured 33% fill by tick-count ratcheting) reproduced against the v1 scripts, where 19 of these checks fail, and passing here.

Developing on it

claude plugin install copies the plugin into ~/.claude/plugins/cache/… — editing this folder changes nothing the hooks run until you refresh that copy:

claude plugin marketplace update whetstone
claude plugin uninstall --scope local strain@whetstone
claude plugin install strain@whetstone --scope local

Note the flag order on the uninstall: as of this writing, `claude plugin uninstall

--scope local` (flag last) intermittently fails to resolve the scope, while `--scope local` before the name works every time. Also: hooks are loaded at session start, so an already-running session keeps executing the previous copy — a live check of new hook behaviour needs a fresh session, or driving the installed scripts by hand with a real payload on stdin. ## License MIT — see [LICENSE](/strain/LICENSE).