Agent Friction
A read-only Developer Tools panel over the agent friction ledger — the health line and the ranked list, closing a problem out, the two-hourly triage loop that dispatches fixers under a daily spending ceiling, and how an agent files a report.
What it is
What keeps costing your agents time — collected, grouped, and ranked.
A read-only Developer Tools panel over the agent friction ledger. When an agent hits a wall — a command that times out, a tool that keeps failing, a guard that blocks it, a runbook that lies — that problem is recorded. Reports describing the same problem collapse onto one row, so forty agents hitting one wall read as one problem with a count of forty rather than forty separate incidents. That count is what tells you where the time is actually going.
Opt-in. Nothing is collected until you turn it on.
Where to find it
Turning it on
Settings → Lab → Agent Friction.
That one switch (agentFrictionReportsEnabled) is the whole feature. It controls what gets
COLLECTED, and whether the panel is shown:
| It gates | Effect when off |
|---|---|
| The sidebar panel | The Agent Friction row does not appear under Developer Tools |
POST /friction |
An agent filing a report is refused, and told the feature is off |
| The fixer loop | The triage schedule is disarmed — nothing is dispatched to fix anything |
| The automatic capture | No friction rows are written. (The separate per-session counters behind Weekly Session Analysis are a different tally and keep running — this switch does not touch them.) |
| The panel's report bodies | Refused, as a backstop for a stale window |
| The panel's ranked list | Still answers — with an empty list and a plain “collection is off”, so the panel says the true thing instead of showing an all-clear |
There is deliberately no second switch. A feature with two flags that both default off is two ways
to describe one state; the single flag is resolved everywhere through one shared predicate
(isAgentFrictionEnabled), so the seams above cannot disagree about whether you opted in.
What it does not do is lock you out of data already on your machine. GET /friction/clusters
and its /reports sibling keep serving your own local ledger, as they did before this feature
existed and as the agent triage tooling still expects. Off means stop collecting and hide the
panel — not make your own data unreadable from your own command line.
Default changed. Collection used to be on for every install, on the reasoning that a friction report costs the user no attention — true of attention, but the automatic capture was writing to every install's database with no way to see it and no working off-switch. It is now off until you choose it.
How it behaves
What the panel shows
The health line — read this first
It sits above the list and it is the reason an empty panel can never mislead you. An empty list has three different meanings, and they are three different sentences:
- "Collection is off" — you have not turned it on. Nothing is being recorded.
- "Nothing has ever arrived" — it is on, but no report has ever landed. That is a broken pipe, not a quiet week.
- "Last report 3 minutes ago" — genuinely quiet. Everything is working.
There are exactly three, and there is deliberately no fourth "this version of the app cannot say" arm: main and the renderer ship as one build, so the health fields can never be missing. An arm for a state the code cannot reach would advertise a distinction it cannot make — its own kind of lie. (This page described that fourth arm for a while; it never existed in the code.)
The same line also tells you whether the fixer loop is running — see Getting problems fixed below.
The same distinction applies to whether anything has ever been closed out. A list that only ever fills and never drains looks identical to a healthy one — both show a full ranking — so the panel states when a problem was last closed rather than leaving you to infer it.
The ranked list
Each row is one problem:
What it is — a short plain-English summary of the problem. The switch at the top of the panel flips every row between Plain English and Technical; the technical version is the exact wording the agent wrote, and it is always one click away because the engineers and the fixer agents need the precise words. Plain English is the default, because the panel exists so that anyone running AI sessions can read it.
The summary is generated once per distinct wording by a cheap model and reused after that, only for the rows actually on screen, and only while the panel is open. It never replaces the real title in the ledger — the title is what groups reports together, so rewriting it would scatter every group. If a summary has not been generated yet, the row simply shows the original wording.
How bad — blocked (could not proceed), slowed down, or annoyance.
How much it cost — total minutes agents reported losing to it.
How often — how many reports have landed on it.
Who saw it — split three ways, and the split matters:
- reported by an agent — a mind judged this worth writing down.
- auto-detected — a known failure seam fired.
- from tool errors — passive telemetry scraped from every tool error and hook block. High volume, and much of it is an agent's own bad path.
A problem made only of tool-error telemetry has nobody vouching for it. The panel says so, because ranking it as equal to a human-authored report would be a false equivalence.
Expanding a problem
Opens the individual reports behind it: what each agent wrote, plus the command and a slice of the output captured the first time this problem was seen. Secrets are scrubbed before anything is stored.
All of this text is rendered as plain text, never as formatted markup — it is written by agents from arbitrary command output, and it is not treated as trustworthy.
Getting problems fixed
Collecting problems is only half the feature. The other half is the triage loop: every two hours it looks at the ranked list, and if something is costing real time and nobody is on it, it starts a session that verifies the problem, writes a fix spec, and dispatches someone to fix it.
The loop turns on with the feature. There is no second switch and no command to run. This was not always true, and the gap was expensive: the dispatcher existed for over a week with nothing in the product scheduling it, so installs collected forever and nothing ever acted. One machine ran it only because a person created the schedule by hand — and that machine closed out 22 problems while every other install closed out zero.
It spends money, and there is a ceiling
The loop starts real Claude sessions, and those sessions start more sessions to do the fixing. On a heavy development machine that has measured at roughly $1,400 a day.
So it ships with a spending ceiling — frictionTriageCostCapUsd, $25 per day by default,
0 to turn the ceiling off. The panel shows what has been spent against it, and you get one inbox
alert when it is reached.
Be clear about what the ceiling can and cannot do:
- It stops the next triage run. That part is enforced — over budget means no new run starts.
- It cannot recall fixers that are already underway. A triage session decides for itself how many fixers to start, and nothing can take back a decision a running session has already made. The remaining budget is written into that session's instructions to keep it in bounds, but that is guidance, not a hard stop.
- Each fixer runs on the repository's own default engine. A fixer is started with no engine named, so it inherits the default you set for that project — the same engine a session you start by hand would use. A triage run never overrides it.
Where to change it: Settings → Lab → Agent Friction, in the panel that appears under
the switch once the feature is on. 0 removes the ceiling entirely.
The older controls are unchanged and still apply: at most one triage run every two hours, a minimum
severity below which nothing is dispatched, a rule that a problem seen only by automated telemetry
never spawns a paid fixer, and the file-based kill switch ~/.amc/disable-friction-triage.
You are told once, when it starts
The first time the loop actually arms on your install, you get a single inbox card saying so, with a one-click button to the spending limit. It fires on the moment the loop starts — not on every launch — so a machine that has been running it for weeks stays quiet.
This exists for one case in particular: if you had already switched Agent Friction on back when it only collected, the same untouched switch starts spending on your next launch. That change is automatic, but it is not silent.
When the loop cannot run at all
On a packaged install the dispatcher script is not shipped and there is no source checkout for it to work in, so the loop genuinely cannot run there. The panel says so in that case rather than showing a healthy-looking idle state — an install that cannot act and one that simply has nothing to do are not the same thing, and reading one as the other is the exact silence this feature exists to end.
What it does not do (yet)
It is read-only. You cannot mark a problem "fixed" or "not fixing" from the panel. That has a real consequence worth stating plainly rather than leaving you to discover: the ranking measures problems that have not been closed, which is not the same as problems that are still open. A problem someone fixed weeks ago keeps ranking until it is closed out.
Closing out is available from the command line:
npm run friction -- --list # the same ranked list this panel shows
and, for a specific problem, POST /friction/clusters/<fingerprint>/resolve with
{"note": "why", "status": "resolved"} (or "wontfix" — the problem is real and we are
deliberately not fixing it; the two are never interchangeable).
GET /friction/clusters?status=resolved reads back only the problems in one state, so you can
confirm a close actually took. A value outside new / dispatched / resolved / wontfix is
refused rather than ignored — an unfiltered answer and a filtered one look the same otherwise, and
that is what previously made a close that had worked look like a close that had not.
A fresh report automatically reopens a closed problem, so closing one out is reversible and safe.
Where the data lives
A local SQLite table in your own install. It is excluded from cross-machine backup merge (a friction ledger is specific to the box that produced it), and the reports never leave your machine unless you send them somewhere yourself.
Usage telemetry records only that you opened the list (and whether it was empty), that you expanded a row, and that a friction command-line route was called. Which problem is never recorded — a friction title is agent-authored text quoting a real command, and putting that in telemetry would defeat the point of keeping the ledger local.
The two panel counters only fire from the panel, which is hidden until you switch the feature on.
The command-line counter is different and worth knowing: it is recorded when the request arrives,
before the switch is checked, so an agent that runs npm run friction on an install where the
feature is off records the attempt and then gets refused. That is deliberate — the number means
someone tried, which is worth knowing — but it does mean a switched-off install is not
guaranteed to report nothing.
For agents
Reporting from an agent
npm run friction -- --category <c> --severity <s> --title "<the PROBLEM>" [--minutes N]
The title is the clustering key, so it must describe the problem, not your incident: "cloud gate never claimed by a VM" clusters with everyone else who hit it; "my test didn't run at 6pm" clusters with nobody and stays invisible.
Already reported? Join that cluster instead of starting a new one
npm run friction -- --list # every row prints its fingerprint
npm run friction -- --category tooling --severity slowed \
--title "<how YOU would describe it>" --cluster tooling:faf827ac0bf9d228
Wording alone decides the grouping, so two agents describing one bug file two half-sized clusters and neither ranks — measured at nine reports of one bug becoming eight clusters. Matching similar wording afterwards was tried and does not work, so you have to say so. Your own title is still recorded; it just stops deciding which cluster you land in.
A fingerprint that is mistyped or names no existing cluster is refused and tells you why — nothing is ever filed into a cluster nobody could find again.
The cluster is in a DIFFERENT category and you are sure it is the same bug
npm run friction -- --category cloud-gate --severity slowed \
--title "<how YOU would describe it>" \
--cluster tooling:c70b2553967bc699 --cross-category
Crossing lanes is refused unless you say --cross-category, because the same paste made by
mistake — a mis-set --category, the wrong --list row — would bury one real problem inside
another and send the fixer to the wrong subsystem. Declared, it is an act you chose, and the two
halves finally rank as one row whose cost and report count add up.
Only use it when you have actually verified the two are one defect with one root cause. Nothing
merges on its own and nothing matches wording for you; you are the signal. Real example: a shared
typecheck cache reported under tooling and the same defect reported under cloud-gate ranked #2
and #4 apart at cost 60 and 50, instead of one row at 110.
The declaration relaxes the lane only. A mistyped key, an unknown lane, and a cluster that does not exist are all still refused.
Reporting raises no notification and interrupts nobody — that is what makes agents willing to do it at all.
Your project is recorded for you — do not try to work it out and send it. The report is filed
against the project of the session that filed it, read straight from your session. You do not pass
it, and you should not: matching your working directory to a registered project is a guess, and a
report filed against the wrong project is worse than one filed against none. If you genuinely know
better than your session does, projectId in the HTTP body still wins — but an empty string does
not count as knowing better, and a report filed with no session at all is recorded with no project
rather than a guessed one.
If the feature is off, the command says so and files nothing. It does not queue the report for later: a refusal is a settled answer, not a hiccup, and queueing it would promise a delivery that could never happen.
Related
- Weekly Session Analysis — the per-session counters and the optional paid weekly AI analysis. Its ingestion detector is what feeds the automatic half of this ledger. The two are tallied side by side but are NOT the same data: its counters record how many errors a session hit and keep running regardless of this switch; the rows here record what they were, and stop the moment you switch collection off.
- Developer Tools — the sidebar group this panel lives in.
Last verified 2026-10-06