Agents repeat themselves quietly, and the invoice is the only place it shows up.
A loop re-reads the same file eleven times. A planner re-asks a question it already answered because the answer never made it back into context. Nothing errors, so nothing alerts.
But a repeat is not automatically waste. Polling until a value changes is correct. Re-reading a file after writing to it is correct. Retrying a failed call is correct. A tool that counts duplicates and calls the total "waste" is guessing.
Three small programs joined by a plain schema, installed as one package.
Each stage only needs the one before it to speak the schema in between, so any stage can be swapped for your own: a source plugin, an analysis plugin, or a report format, with no fork and no PR against the repo.
A local OTLP receiver. Writes whatever telemetry it's sent to disk, nothing more. Optional. Skip it if your exporter already writes files.
Turns one framework's raw telemetry into the common event schema. Tells the sources apart from the data itself; --source forces one.
Pairs up repeated calls from that schema and classifies each pair into one of six buckets. Doesn't know or care where the input came from.
One self-contained file that opens with its own coverage line. Or --format json for anything downstream.
Three of these are verdicts. Three are only ever surfaced for review.
Keeping those two things apart is the point. Full reasoning for each bucket lives in docs/schema.md and in classify.py's module docstring.
The call repeated, the result didn't change, nothing wrote to state in between, and the task still failed.
Drop any one and this is a guess, not a finding.
ACTIONCache the result or guard the retry. This spend bought nothing.
A specific reason it isn't waste: the result changed (polling worked), a write intervened (verification), or the task succeeded.
And neither the result nor the write status already confirms waste on its own.
ACTIONLeave these alone. Cache them and you'll break polling and verification.
A required signal (the result, the write status, or the task outcome) was missing from the trace.
Counted and named rather than guessed at.
ACTIONEmit result hashes and task outcome, then re-run to get a verdict.
Each source has a genuinely different OTLP shape, and a different blind spot.
Four frameworks work with redundo adapt today: Hermes and anything OpenInference-instrumented (including the OpenAI Agents SDK and Google ADK), Claude Code across the CLI, IDE extensions, and Agent SDK, Claude Cowork, and OpenClaw. Adapter quality is bounded by what each framework actually emits.
Pipe your own NDJSON matching the schema straight into analyze, or write an adapter plugin. No PR against the repo required.
Every number is checkable, and nothing leaves the machine.
The only service involved is redundo collect, an OTLP receiver you run yourself that writes to a directory you name. No account, no upload, no hosted anything. The report is a single file that renders with the network off.
Repeats are matched on content_hash equality, and near-duplicates on a SimHash fingerprint, not on stored prompt text. What each hash can and cannot support is written down rather than asserted.
Where a framework can emit raw content, the switch is yours and defaults to off: captureContent and captureIdentifiers on the OpenClaw plugin, OTEL_LOG_* on Claude Code. More signal or less; stated either way.
Every report opens with how much of the trace it could actually price (64% in the demo above) and excludes unpriced events from every dollar figure rather than estimating them into the total.
When a source doesn't report cost_usd, cost comes from a bundled per-model pricing snapshot: local, inspectable, and updated on your command with redundo update-pricing.
Reports list sample cases by task and step, like task=session-001 step=2, so any count can be checked by hand against the trace that produced it.