---
title: Agent Friction
---

# Agent Friction

## 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](session-forensics.md) 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.

**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](session-forensics.md) — 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](developer-tools-group.md) — the sidebar group this panel lives in.
