✅ 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.
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.
🟡 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.
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:
strain:start skill holds the same steps, under a name that
matches the moment.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.~/.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).What strain cannot control:
Strain measures one conversation with a person in it. Two kinds of run are not that, and strain now leaves them alone:
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).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.| 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.
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 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:
stale until
the next request writes a new one (the zero receipt Codex writes right after a
compaction is skipped). There is no clock: an old receipt is not stale by age.CODEX_THREAD_ID, which is the same id; record commands use
it as their session key.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.
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:
--session <id>, typed.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.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.
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.
strain-sign.sh --receipt that it now says “this shell can write”.
It never approves the permission itself and never widens it. Moving the folder with
STRAIN_STATE_DIR works only if the host’s hooks get it too: set in the agent’s shell
alone, the records land in one folder and the hooks keep reading another.Log.strain, when you sign with one) is the only file strain
writes inside a project, and only where you point it.lines.json (0.11.0) is the one file in strain’s folder that you write and strain
only reads — see Your own lines at fill levels.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:
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.”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.
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.
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.)
strain-sign.sh --agent ana --seed 12. A seed at or below a
number the book already gave is refused, and nothing is booked.strain-sign.sh --continue <previous session id>. The row
records "continues", and it is not counted as a clash.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 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.
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.
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"}]}
STRAIN_LINES (a path) if set, else lines.json in strain’s folder. A link
there to a file you keep elsewhere works. No file: strain behaves exactly as before.mark (optional): backup or wrap-start — strain then adds the exact command
that records it (strain-wrap.sh --backup or --start, for this session). When the
session already holds that step, the line keeps its words and says when instead
(0.11.1): a wrap already started → its time and call, and no command; a backup on
record → its time and fill, and the command stays (an old backup may be stale). A line
is never hidden.at a number above 0 and at most 100, each at most once; say one line of
text, at most 300 characters; at most 20 lines. A file that is there but breaks a rule
(or a link to a file that is gone) is not used, and the next regular tick says so once,
naming the file and the reason. Nothing in the file can stop the counter: whatever goes
wrong in this step becomes that note. Two at values that differ are two lines, however
close.crossed (per line: when, at what fill and
call, and whether it was said or jumped) and peakPct / peakAt (the highest trusted
fill so far — the current reading falls after a compaction).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.
Context occupancy, when measurable. Plus behaviour, always:
strain-signal.sh <kind> --caught|--escapedHard 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:
UNMEASURED — never “Healthy” — with the command to feed the host’s own reading.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.
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 |
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.
| 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:
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.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.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.
strain-report.sh only writes a draft; sending it is yours to do, or not.hookSpecificOutput.additionalContext on
stdout IS surfaced; plain stderr/stdout prose is not. On other hosts this remains
unverified — if you never see a strain readout, check the channel first.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.
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.
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