Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OMNI

Your AI agent pays to read the same output over and over. OMNI stops that.

One small program between your terminal and your agent. Local, no API key, no proxy. Install it and you never type its name again.

brew install fajarhide/tap/omni && omni init

Inside Claude Code, two lines and the agent does the rest:

/plugin marketplace add fajarhide/omni
/plugin install omni@omni

What that buys, measured

a file your agent reads twice97.2% off the second read
git log -1594% smaller, every commit kept
cargo test, 490 passed and 10 failed92.9% smaller, the failures kept
build and test output across the corpus78.0%
the tool definitions in every request4,940 bytes lighter

Every one of those replays on your own history. That is the point of the rest of this page.

The problem, in one screen

Your agent runs a test suite. Four hundred lines come back, one of them matters.

$ cargo test
    Compiling omni v0.7.5
     Running unittests src/lib.rs
running 412 tests
test pipeline::scorer::tests::scores_errors_critical ... ok
... 409 more lines of "ok" ...
test result: FAILED. 411 passed; 1 failed

The failure survives. The 406 lines of ok do not. A handle on the last line brings every one of them back, byte for byte, if anything ever needs them.

The part nobody else does

Filtering noise is the easy half, and several tools do it. Here is the harder half, and it is where most of OMNI’s saving comes from.

Your agent reads a file. Three turns later it reads the same file again, because nothing remembered the first read. You pay full price both times.

OMNI remembers. The second read comes back as one line:

[OMNI: 178 lines already shown, omni retrieve 0000000000000000]

A 7.6 KB file read twice costs 7.6 KB and then 214 bytes. Nothing was deleted: those lines are already in your agent’s context from the first read, so sending them again buys nothing. The handle is there in case they scroll out of reach.

This is the ledger, and on real command histories it does more work than every filter combined.

Prove it on your own machine

Most tools in this space ask you to trust a number from someone else’s laptop. Run these instead:

omni stats                     # what OMNI did on your history, in counted bytes
omni retrieve <handle>         # any handle from any marker, printed back byte for byte

Every figure on this site comes from a corpus you can rebuild. Benchmarks has the method and the exact command for each row, including the comparison we lose.

What you get

Longer sessionsLess context spent on ceremony means more turns before you hit the wall, and fewer compactions that lose your thread.
Lower bills14.9% fewer bytes across 6,656 real commands. On file reads, 25.0%. On git, 22.1%. On build and test output, 78.0%.
Nothing lostEverything removed is archived locally. omni retrieve <handle> prints it back.
Nothing inventedIf OMNI cannot understand output, it hands it back untouched rather than guessing.
Memory between sessionsClose your editor, come back tomorrow, switch from Claude Code to Codex: the project context is still there.
Nothing to changeNo proxy, no API key, no command to prefix. Install it and use your terminal normally.

Where it actually helps

Where OMNI helps walks through the situations with the real numbers attached, including the ones where it does nothing and why that is correct.

Start here

Just want it working. Install takes about five minutes. Then read Reading the markers, which is the one page worth your time, because the markers are how OMNI tells you what it did.

Want to understand it first. What OMNI is, then How it decides what to cut, then The ledger.

Three things it will not do

It will not send anything anywhere. Every stage runs on your machine and the archive is a SQLite file in your home directory.

It will not sit between you and your model. There is no proxy and no API key handed to a local process. That was decided against on purpose, and the reasoning is written down.

It will not quietly guess. A stage that failed to understand its input hands the input back unchanged. Structured data like JSON and YAML is never touched at all. Anything removed leaves a marker saying so. Those three rules outrank compression, in that order, every time they conflict.

The honest version of the numbers

Across 6,656 real commands, 97.3% of calls saved nothing at all, because there was nothing to save. A two-line git status has no ceremony to drop and no repeats to fold, so OMNI hands it straight back rather than inventing a saving to report.

The 14.9% is what is left after counting all of those zeroes. It is a real average over a real mix, not a best case picked from a good day.

We publish the comparison we lose, too: on filtering alone, rtk gets 6.2% on that corpus and OMNI gets 2.7%. It is the ledger that puts OMNI ahead overall, and running rtk’s filters with OMNI’s ledger would beat both.

If you want a number that describes your machine rather than someone else’s, run omni stats after a few days.

Where to ask

Discord for questions, and especially for the case this project cares about most: OMNI stating a result its input does not support. The issue tracker works too. A report with the raw and distilled output side by side gets fixed either way.

What OMNI is

A small program on your machine that edits what your AI agent reads, before the agent reads it.

That is the whole idea. Everything else on this page is about the rules it follows while doing it, and the rules are more interesting than the editing.

The problem it exists for

An agent working in a terminal spends most of its context on output nobody chose to send it:

  • a test run is 400 lines of ok and one line that matters
  • a build is a compile log wrapped around a one-word verdict
  • a file gets read, then read again three turns later, because nothing remembered the first read

None of that is free. It fills the context window, which ends your session sooner, and you pay for it again every time the conversation is compacted.

The obvious fixes are all worse than the problem:

The fixWhy it fails
Truncate long outputIt cuts the end, and the end is where the verdict lives
Ask a model to summariseAn inference call per command, and a summariser that can be wrong
Tell the agent to be carefulWorks until the agent is busy, which is always

OMNI is the fourth option: a program that knows what cargo test output looks like, remembers what your agent has already been shown, and never guesses when it is unsure.

Where it sits

Every serious agent host can run a program when a tool finishes and use what that program returns. Claude Code calls it a PostToolUse hook, Cursor and the others have their own name for the same idea. OMNI installs itself there, and in the matching slot before the tool runs, which it uses only to hand a matched command to itself. The command still runs unchanged; the shell never knows.

Two consequences follow from that position, and they are the reason this shape was chosen over a proxy.

It sees output, not requests. Your API key never passes through it, no request is delayed waiting on it, and if it dies the host carries on with the raw bytes.

It cannot help where the host will not let it. A host that does not apply a hook’s rewrite to its built-in shell tool will show the agent the same bytes no matter how good the filters get. That is not a bug to fix in OMNI, it is a property of the host, and Supported agents says which host is on which tier.

What it does to a command

Four things, in order, and any of them may decide to do nothing:

  1. Refuse. JSON, YAML, base64, terraform plans, anything a later step is going to parse: handed back untouched. See What it refuses to touch.
  2. Filter. A distiller that understands this tool keeps the verdict and the failures and drops the ceremony. There are 12 of them, covering build, test, git and other version control, search, cloud, database, JavaScript and TypeScript tooling, file reads, security scanners and system operations, plus a generic fallback.
  3. Collapse. Long runs of near-identical lines become one line saying how many there were.
  4. Fold. Lines the agent has already been shown become a handle instead of a repeat. This is the ledger, and on real corpora it does more work than the filters do.

Then the raw input goes into the archive, and the agent gets the result plus a marker saying what happened.

If you would rather see this as situations than as stages, Where OMNI helps has six of them with the measured saving on each.

What it does to itself

Everything above is about output. There is a second thing OMNI edits, and for a long time it did not edit it at all: its own weight.

OMNI registers as an MCP server, and tool definitions sit in the prefix of every request of every session it is attached to. A prefix byte is not paid once. It is carried from the first request and re-read on every one after it, where a byte removed from tool output was inserted somewhere in the middle and is read fewer times.

Measured across 229 sessions, sixteen of the twenty-five tools OMNI advertised had never been called once, and those sixteen were 4,940 bytes. The distillers remove a median of 4,942 bytes from tool output in a session that pushes real volume through the hook. Two bytes apart, and the prefix side is the one carried from the start.

So OMNI now tells a host about the tools its tier actually uses. A tool that spends as much context describing itself as it saves is not a token-efficiency tool, and noticing that required pointing its own measurement at itself.

What it is not

Not a compressor. It is not trying to make output small. It is trying to make output that an agent can act on, next to a number a human can check. Those pull in different directions more often than you would expect, and when they conflict the number loses.

Not a summariser. No model runs inside the pipeline. The budget for a hook is single-digit milliseconds and nothing with an inference call fits in it.

Not a memory product, though it has one. omni remember, omni goal and the session handoff exist because the same agent that reads too much also forgets everything between sessions. Memory across sessions covers that half.

The rule it is most serious about

A stage that recognised nothing hands back what it was given.

The failure this project keeps having to fix is not lost bytes. It is a confident summary of input that was never parsed: a find that reported 99% saved by throwing away the file paths that were the answer, a cargo test that said 1 passed about a run cargo itself called 490 passed, a dev server reported as a passing test suite.

Every one of those compressed beautifully. All of them were wrong. So the trait that every distiller implements returns Option<String>, and a distiller that failed to parse returns None and the caller hands back the raw bytes. It is enforced by the type rather than by the author remembering.

Where OMNI helps

Eleven situations, with the measured number attached to each. Two of them are cases where OMNI does nothing, and those are in here on purpose: a tool that claims to help everywhere is a tool nobody can predict.

Every figure comes from the same replay of 6,656 real commands described in Benchmarks, so they are averages over a real mix rather than a good day picked out of a log.

1. The agent keeps re-reading the same files

The situation. You ask for a refactor. The agent reads auth.rs, wanders off to check a caller, comes back and reads auth.rs again. Six turns later it reads it a third time. Every read is charged at full price, and none of the repeats told it anything the first one did not.

What OMNI does. The second read comes back as a marker with a handle. The lines are already in the agent’s context; sending them again is paying twice for one fact.

The number: 25.0% off file reads across the corpus, and up to 97.2% off a single repeated read of one file.

This is the biggest single win in the whole product and it is invisible while it works, which is why the marker exists.

2. A test suite fails and you cannot see why

The situation. 412 tests, one failure, and the failure is on line 388 of the output. Your agent reads all 412 lines to find it, and if the run is long enough the host truncates the tail, which is exactly where the verdict lives.

What OMNI does. The test distiller keeps the tally and every failure with its assertion and file position, and drops the passing lines.

The number: 78.0% off build and test output.

This is the case where filtering, not the ledger, does the work. Test output is enormously repetitive within one run, so there is real ceremony to remove before anything has been seen twice.

3. git log and git diff fill the screen

The situation. One commit’s Author, Date and wrapped body is five lines. Fifteen commits is a screen and a half, and your agent wanted the subjects.

What OMNI does. Every commit is kept, as one hash subject line. Nothing is summarised away and no commit disappears; the envelope around each one goes.

The number: 22.1% across git and gh on the corpus, and 94% on a verbose git log -15 specifically.

4. Your session dies at the context limit, repeatedly

The situation. Long debugging session, and about two hours in the conversation compacts. The agent loses the thread, re-reads files it had already understood, and you re-explain the task.

What OMNI does. Two things. Less context spent per command means the wall arrives later. And memory across sessions survives the compaction: project knowledge, recurring error patterns, and the goal you pinned with omni goal are in SQLite, not in the context window.

The honest limit. OMNI cannot stop a compaction, and at the moment one happens it deliberately forgets what it had shown you, because the licence to replace lines with a handle is that the agent is still holding those lines, and compaction is when that stops being true.

5. You switch agents, or machines, mid-project

The situation. You start in Claude Code, move to Codex CLI for a change, and both of them start from nothing.

What OMNI does. The store is one SQLite file keyed by project path, not by agent. A second agent working in the same directory reads the same project knowledge, and the ledger’s project scope will hand it a handle for output an earlier session already produced. That marker says not shown here rather than already shown, because this agent has genuinely never seen those bytes and the wording has to be true.

The honest number. Cross-session repetition is 3.7% of post-filter bytes against 19.1% within a session, so this is worth about a fifth of the in-session saving. It is real, and it is not the headline.

The honest caveat. Two agents in one repository share that history by side effect rather than by design. The marker used to say from an earlier session, which reads as your earlier session when it was someone else’s, and worse, as a claim the content had already arrived; it now says not shown here. The ledger is straight about what is and is not keyed on the agent today.

6. kubectl get pods -o json | jq

The situation. You pipe structured output into something that parses it.

What OMNI does: nothing. JSON, YAML, NDJSON, CSV and TSV pass through byte for byte. A compressor that reformats a payload the next command is about to parse has not saved you anything, it has broken your pipeline.

The number: 0%, by design. See What it refuses to touch.

7. You read one big file in several passes

The situation. A file is longer than one read, so the agent takes it at an offset, then another, then another. Each window repeats the head of the file, because that is what a window at an offset contains.

What OMNI does. It folds the repeated head and moves the line numbering to match, so the lines you can still see are numbered where the file really has them. That second half matters: a fold that renumbers what is under it is worse than no fold, and it is why this case was refused for a release until the numbering could be kept true.

The number: 0.0% before, 4.7% after, measured on four overlapping windows of one markdown file. Source files are unaffected, since the readfile distiller reaches those first at 46.6% either way.

8. You dispatch a subagent

The situation. Your agent spawns a helper to do a scoped job. The helper starts with an empty context and reads a file the parent already read.

What OMNI does. It gives the helper its own view. Claude Code hands a subagent the parent’s session id, so a ledger keyed on the session alone would answer the helper with the parent’s history and tell it 200 lines were already shown, about bytes that context had never received. The helper now sees either the content or a marker that says plainly nothing was shown here.

The number: no ratio, and that is the point. This is a correctness case. The saving was never the problem; the claim was.

9. You follow a marker to get the content back

The situation. A marker says omni retrieve <handle>. You run it, or your agent does, and reads the result.

What OMNI does. It hands those bytes over whole. Before, they went back through the pipeline, hashed the same, and were folded into the very marker that sent you there, so following the instruction returned the instruction.

The number: one delivery, not an exemption. The next repeat folds again, which matters because 15.05% of the archive on a real installation has been pulled at least once, and exempting all of it would trade a false claim for a lost saving.

10. Your context gets compacted mid-session

The situation. The session runs long, the host compacts the conversation, and half of what your agent was holding is gone.

What OMNI does. It forgets. The ledger’s whole licence is that the agent still holds the bytes a handle replaces, and compaction is where that stops being true, so the shown-set goes with it. Nothing after a compaction claims you have already seen something you no longer have.

The number: no ratio. It costs savings on purpose, and it is the trade that keeps the markers true.

11. Every request carries a tool list you never call

The situation. OMNI registers as an MCP server, and tool definitions sit in the prefix of every request of every session. Unlike output, a prefix byte is not paid once: it is re-read on every request after the first.

What OMNI does. It advertises the tools your host’s tier actually uses, nine instead of twenty-five, with OMNI_MCP_TOOLS=all to restore the rest and omni doctor naming which set is in force.

The number: 4,940 bytes off every request. Measured across 229 sessions: sixteen of the twenty-five had never been called once.

And one more where nothing happens

kubectl get pods with 35 pods returns a table where every row is a fact. There is no ceremony to drop and nothing has been seen before, so OMNI hands back all 35 rows and reports a 0% saving.

97.3% of all calls in the corpus are like this. That is the number worth internalising: OMNI is not a thing that shrinks everything a little, it is a thing that does nothing most of the time and a great deal occasionally. The 14.9% aggregate is what is left after every one of those zeroes is counted in.

What this adds up to

Class of commandCalls in the corpusSaved
build and test6978.0%
file reads69925.0%
git, gh66122.1%
search (grep, rg, find)82813.3%
infra (kubectl, az, docker)2548.2%
everything else4,1456.9%
all of it6,65614.9%

Run omni stats after a few days and you get this table for your own history, which is the only version of it that describes your work.

How it decides what to cut

The pipeline is fixed and every payload walks the same stages:

Read → Guard → Score → Distill → [Collapse] → Ledger → Route → Persist

None of them is allowed to invent anything, and each one is allowed to decline. Collapse is bracketed because it is a fallback rather than a step: it runs only when the distilled form failed to beat the guardrail. The pipeline, stage by stage has the diagram and the reasoning.

Guard

The gate. It answers one question: is this payload something a later step is going to parse? If yes, nothing downstream runs and the bytes come back exactly as they arrived. What it refuses to touch is the whole of this stage and it is worth its own page, because “OMNI did nothing” is usually this working correctly rather than a failure.

Score

Every line gets a relevance tier. The scorer is a pure function of the text, the command that produced it, and whatever session history exists.

tierweightwhat lands here
Critical1.0errors, failures, the verdict line, anything naming a file and a line number
Important0.7warnings, counts, state that changed
Noise0.1progress, timing, decoration, repeated ceremony

The tiering happens before any distiller sees the block, which matters when you are debugging why a distiller behaved oddly: the tier may already have decided the outcome, so probe the segment tiers before rewriting the distiller.

Distill

Now a tool-specific filter runs, chosen by matching the command. The cargo test distiller keeps the counts and every failure with its assertion. The git distiller keeps the changed paths. The search distiller keeps the match lines with their filenames.

Each one implements the same trait, and the signature is the design:

fn distill(&self, segments: &[OutputSegment], input: &str,
           session: Option<&SessionState>) -> Option<String>;

Option, not String. A distiller that did not understand its input returns None and the caller hands back the raw bytes. That is the difference between “I read this and here is what matters” and “I recognised nothing and here is a confident summary of it”, and it is enforced by the compiler for all 12 rather than by each author remembering to check.

Collapse

Runs of near-identical lines become one line stating the count. Twenty Downloading foo v1.2.3 lines become one.

Two things about this stage surprise people. It runs after the distiller and only when the distiller did not earn its keep: both hooks distill the raw bytes, ask beats_guardrail, and reach for the collapsed form only if that fails. So a distiller always reads the original output, never [N similar lines collapsed] markers. And which collapse mode fires is chosen by specificity, so a kubectl command piped into grep may take the infrastructure path rather than the log path.

Ledger

Everything above judges this payload on its own. The ledger is the one stage that judges it against what the agent has already been shown, replacing a run of repeated lines with a marker and a handle. It is the largest single source of savings and it has its own page: The ledger.

Persist

The raw input is archived, keyed by SHA-256, and the marker the agent sees carries a handle into that archive. Covered in Nothing is deleted.

Archiving happens even when the projection saved nothing. A block is worth remembering because it may be seen again, not because it compressed today.

What decides the order

Correctness beats compression at every stage, and the order they win in is written down:

  1. Never fabricate. A stage that recognised nothing hands back what it was given. A failed command passes through verbatim. Structured payloads are never touched.
  2. Never lose the answer quietly. Anything dropped leaves a marker, and where the content allows, a handle that retrieves it.
  3. Then compress, as hard as the first two allow and no harder.

The reason that ordering is explicit is that the project has broken it before. A kubectl table once came out as k8s: 2 pods because a pod table is an enumeration where every row is a datum. It reported a large saving. There was no noise in the input to remove, so the saving was the answer.

Nothing is deleted

Every byte OMNI removes is written to a local SQLite archive first, keyed by its SHA-256. The agent gets a marker carrying a 16 character handle, and the handle brings the original back byte for byte.

[OMNI: 406 lines omitted, omni retrieve 0000000000000000 for full output]
omni retrieve <handle>

That works from any shell, in any session, on any host, and it does not re-run your command. Where MCP is wired, the agent can do it itself with the omni_retrieve tool without asking you.

Why this is the load-bearing rule

Filtering output is a bet that the removed part did not matter. The archive is what makes the bet safe to lose. It changes the worst case from “the answer is gone” to “the answer costs one retrieval”, and that difference is what lets the rest of the pipeline be aggressive at all.

It also changes what a bug means here. A distiller that cuts too much is a bad trade. A handle that does not resolve is a broken promise, and it is the one defect this mechanism cannot have.

The one rule the archive enforces on everything else

A run is archived before its marker is written, and a failed archive means the run stays verbatim.

The order matters. Writing the marker first and archiving second would produce, on any write failure, a marker pointing at content that was never stored: output that looks like it can be recovered and cannot. That happened once, store_rewind returned a key even when the write had failed, and the fix was to make the marker conditional on the archive rather than the other way round.

So when you see a handle, the content behind it exists. That is not a hope, it is the order of two statements.

What it costs

Disk, and a write on every distillation that removed something.

The archive is capped rather than unbounded: archiving every lossy distillation measured 83.1 MB over 30 days, and capping the archived block at 64 KB brought it to 13.3 MB while still covering 3,604 of 3,657 rows. The cap was chosen from that measurement rather than picked.

Traces used for benchmarking are pruned separately, at seven days by default. That prune is why no published figure here can be re-derived after a week, and why every number in Benchmarks names the window it was measured in.

Where it lives

~/.omni/omni.db, a single SQLite file. It never leaves the machine.

omni stats            # what it has been doing
omni diff             # the last command, raw against distilled
omni retrieve <handle>

omni diff is the quickest way to develop trust in this: run a noisy command, then look at exactly what the agent was handed instead.

The ledger

Every distiller answers the same question one command at a time: given this output, what can be dropped.

The ledger answers a different one: given everything already shown in this session, what is this output repeating.

The two are orthogonal, and on real corpora the second one is worth more. Replayed over 6,656 traces, 22.9% of raw bytes were lines the agent had already been shown, and 22.4% still were after every distiller had run. Filtering barely dents repetition, because repetition is not noise. Each line is perfectly good signal. It is just signal that was already delivered.

What it does

A run of consecutive lines that were all emitted earlier becomes one marker naming the count and a handle. Everything else passes through byte for byte.

[OMNI: 40 lines already shown, omni retrieve 0000000000000000]

It reaches the class nothing else can. File reads are the largest class in the corpus, and the filters save 0.0% of them, correctly: you cannot strip lines from a file the agent asked to see without guessing which parts it meant. The ledger takes 25.0% of that same class without guessing anything, because those lines were already delivered once.

Two scopes, two different claims

They are not the same statement and the marker says which one it is making.

originmarkerwhat it means
sessionN lines already shownthe agent is still holding these bytes, so the handle is free unless it chooses to re-read
projectN lines not shown herethese went to a different session of this project and this agent has never seen them

The distinction is the whole reason the project scope exists. An earlier design cancelled it on the grounds that a handle for another session’s content is a lie, which was right about the wording and wrong about the remedy: the fix is to stop saying “already shown”, not to stop remembering.

Because the project claim is not free, it carries a higher bar. A session-origin run must save 150 bytes over its marker; a project-origin run must save three times that, since the agent has no choice about paying a retrieval if it needs the content.

The two floors that decide nothing folds at all

Both bars above ask whether a run outgrows the marker replacing it. Two floors are checked before either of them, and between them they explain most of the cases where output comes back untouched and looks like the ledger is off.

Output under 264 bytes never reaches the ledger. Below that there is no run long enough to be worth a handle, so the whole stage is skipped.

A fold that covers the entire output needs 1024 bytes. The bars assume the agent still holds the rest of the output beside the marker and can decide whether the handle is worth spending. Cover everything and there is nothing beside it, so needing any part of the payload costs a retrieval the agent had no say in. Every whole-output fold this machine recorded was under 1 KB, and four of the four were retrieved within nine seconds, against a 0.85% retrieve rate across all 5,178 distillations in the same store. They saved 2,680 bytes, then spent 319 bytes of marker plus four extra tool calls handing back the same 2,999. The floor is the top of that measured range rather than a knee, because nothing above it was observed either way. n=4, one machine.

The premise everything else follows from

The agent is still holding these bytes.

That single statement is what licenses replacing forty lines with a handle. Every rule below is either a consequence of it or a defence of the moment it stops being true. When you find yourself asking why the ledger does something, ask what it would take for the premise to be false, and the answer is usually there.

It is also why this is a cache invalidation problem and not a memory system. The ledger does not store knowledge. It stores receipts.

The three readers the premise fails for

Every rule worth knowing here is a defence of the moment the premise stops being true. There are exactly three readers it fails for, and the ledger answers each differently.

A subagent. Claude Code hands a helper the parent’s session id, so a ledger keyed on the session alone would answer it with the parent’s history and claim 200 lines were already shown to a context that had received none of them. The scope is the reader, not the session, so a helper accumulates its own and falls through to the project scope for anything else, where the wording says plainly that nothing was shown here.

A context that was compacted. The host says so before it happens, and the ledger forgets that session’s shown-set at that moment. It costs savings on purpose. Nothing after a compaction claims you already have something you no longer hold.

A reader following a handle. Asking for bytes back is proof the reader does not have them, so the delivery answering a pull is handed over whole. Before, it went through the pipeline, hashed the same, and came back as the very marker that sent the reader there. One delivery, not an exemption: the next repeat folds again.

The pattern is worth more than the three cases. When the ledger surprises you, ask which reader is holding the bytes, and whether anything told OMNI that reader had changed.

The flow, one command at a time

Structured payloads never get this far: the same format sniff that gates collapse gates this stage too.

Two details are easy to read past and are the whole correctness story.

The archive happens before the marker, so a handle never names content that was not stored. And what gets recorded is what was delivered, not what arrived: a run that became a marker never reached the agent, so recording it would let the next occurrence claim already shown about bytes nobody received. That was a real defect (#465) and it cut both ways, because session origin charges a third of what project origin does, so the false claim also made the ledger three times more willing to fold.

How it remembers

Three verbs, and each one is a different table or a different trigger.

Store

Two tables, on purpose.

holdssize
ledger_lines(scope, line_hash, ts, agent_id)16 bytes of hash per line
rewind_storethe actual bytes of a folded run, keyed by their SHA-256the content, once per distinct block

Recording every emitted line is cheap because the line itself is never stored, only its hash. The content only goes to the archive when a handle is actually issued.

The hash is taken on the trimmed line, so the same line reached through sed -n and through cat is one line rather than two.

Recording is unconditional; folding is not. A block is worth remembering because it may show up again, not because it compressed today. So a command whose output is entirely new still writes its lines, and pays for itself the next time.

Retrieve

omni retrieve <handle>

An exact lookup on a content address. There is no candidate set, no ranking, no merging of results, and no search: one handle names one block of bytes. The handle is derived from the content, so identical output is one row however many commands produced it.

Nothing is ever pulled back automatically. The marker is a pointer, and the agent decides whether the content is worth a retrieval. That is the trade the whole design rests on: the worst case is not “the answer is gone”, it is “the answer costs one round trip”.

Where MCP is wired the agent calls omni_retrieve itself. Otherwise it runs the shell command the marker printed.

Forget

Time, plus one event.

At compaction, the session scope is dropped entirely. Compaction is the moment inside a session where the agent stops holding what it was shown, so every claim the session scope could make becomes false at once. Forgetting costs a missed reduction. Not forgetting means telling an agent it has content its context no longer contains, which is the defect, not the cost.

At 30 days, both scopes prune on the same window. A session scope cannot outlive its session, so the ordinary retention window already bounds it. The project scope is the one that could grow without limit, and the honest bound on it is the same window: content nobody has produced in a month is content this project has stopped emitting, and a handle for it buys a retrieval of something the agent will not recognise either.

A repeat refreshes the timestamp rather than being ignored, so output that is still being produced does not age out on the strength of when it was first seen.

There is no eviction by size, and that is deliberate. Evicting by size drops the oldest rows of the busiest project first, which is exactly where the repeats are.

What two agents in one repo share

The session scope is one agent’s, because a host session id belongs to one host. The project scope is keyed on the working directory and nothing else, so two agents running in the same repository write into one history and read from it.

That is sharing by side effect rather than by design. Nothing in the ledger knows which agent it is talking to, so a project-origin marker can hand agent B a handle for lines only agent A was ever shown. The higher bar means the trade is priced as a retrieval either way.

The wording used to make that worse. from an earlier session states where the lines came from, and a reader took it as your earlier session, which it need not be, and then as a claim they had already seen the content. A run marker now says not shown here and states the only thing the reader has to act on, which is that these bytes never arrived (#567).

As of #509 the agent is recorded on every line, and nothing keys on it yet. The measurement decides that: keying the scope on (project, agent) would end the cross-agent case together with whatever reuse in it is genuinely free, and the corpus says the effect is currently latent rather than live. The column is what makes it possible to ask.

The rules it inherits

Append-only. It only ever shortens the output of the command in flight and never rewrites anything already delivered. That is what keeps the upstream prompt cache intact: a cache works on a prefix, so shortening the suffix costs nothing while retroactive compaction would destroy it.

Deterministic. The same ledger state renders byte-identical output. The handle is a content address and carries no timestamp. An earlier design used {timestamp}_{hash} and made 4 of 73 repeated inputs emit different bytes.

Nothing is lost. Stated above and enforced by the order of two writes. The general rule and what it costs are in Nothing is deleted.

Failures are never folded. A line stating a failure is exempt however often it has been shown. “You have seen this already” is sound for informational lines and wrong for the error channel, where the repetition is the signal: the same TypeError on a re-run means the bug is still there. Eliding it delivers source context and no statement of what went wrong, which an agent reasonably reads as the failure being fixed. Marking the line unseen rather than filtering it afterwards also splits the run around it, so the frames either side still fold.

Unknown means untouched. Structured payloads never reach the ledger at all.

What it is worth

From the same replay, the ledger is 12.2 points on top of OMNI’s own filters and 11.4 points on top of a competitor’s, which is the clearest statement that it is orthogonal to whose patterns run:

bytessaved
omni, filters only6,469,047 to 6,292,8562.7%
rtk pipe6,469,047 to 6,067,0126.2%
lean-ctx compress6,469,047 to 6,073,7576.1%
omni, with the ledger6,469,047 to 5,506,62714.9%
rtk pipe + omni’s ledger6,469,047 to 5,333,48317.6%

The last row is deliberate. A reader who wants the largest possible number would run their filters with our ledger, and saying so is cheaper than being caught not saying it.

What it refuses to touch

Before anything else runs, the payload is classified. If it looks like something a later step is going to parse, the whole pipeline stands down and the bytes come back exactly as they arrived.

Four kinds are recognised: JSON, YAML, CSV and TSV. Recognising any of them ends the matter.

This is the stage people mistake for a failure. kubectl get pods -o json coming back at full length is not OMNI missing an opportunity, it is OMNI declining one.

Why declining is the right answer

A distilled JSON document is not a smaller JSON document. It is a broken one. The jq two steps later fails, the agent reads the failure, and the cost of that round trip is larger than anything the compression could have saved.

So the gate is deliberately biased. Bracketed but unparseable input, truncated JSON, JSON carrying comments: all treated as structured. Compression cannot repair a malformed payload but it can certainly make it worse.

How it decides, and where it has been wrong

JSON: a whole document that parses. Above a size threshold a full serde_json parse would blow the latency budget, so bracket shape alone decides. Free text almost never carries "key":, which is the cheap signal for the ambiguous cases.

YAML: key-shaped lines, plus one rule that exists because of a real failure. A block scalar (config.hcl: |) hands the rest of the block to whatever the value happens to be: Vault HCL, a shell script, a PEM certificate. Those lines carry no key: and are not YAML-shaped, so a naive sniff calls them prose. One embedded ConfigMap sank a whole 608-line kubectl kustomize manifest that way: the sniff said “not YAML”, the gate stood down, and the manifest went down the lossy path. Lines introduced by a block indicator are now skipped rather than judged.

CSV and TSV: a consistent delimiter count across a minimum number of rows. One row proves nothing.

Turning it off, and when to

OMNI_PASSTHROUGH=1 <your command>

Skips the pipeline entirely. Use it when you are debugging OMNI itself and need to see what a command really printed, or when reading a file whose exact bytes matter.

The prefix works on every path, including inside an agent, but not for the reason it looks like. A hook is a separate process the host spawned, so it inherits the host’s environment and never sees a variable you assign in front of a command. What it does see is the command string, so OMNI reads the assignment there. Two consequences worth knowing: only a leading assignment counts, the same position a shell would apply it in, and echo OMNI_PASSTHROUGH=1 mentions the name without setting anything and is still distilled. Exporting it for the whole session works the ordinary way.

This is the single most useful environment variable here, and it is the first thing to reach for when you suspect OMNI has changed something it should not have. If the output is identical with and without it, OMNI was not involved.

Things that look like this gate and are not

Negative savings on small output. A short payload can come back a few percent larger, because the marker costs more than the compression saves. Expected, not a defect.

A command whose output arrives intact anyway. Around 97% of calls save nothing at all, because there was nothing to save. That is the pipeline working.

kubectl binary streams. SPDY corrupts those with or without OMNI in the picture.

Shell quoting. Word splitting is your shell, not this program.

What it costs

Not zero. Here is the whole bill.

Latency

Median of 12 runs each, release binary, measured end to end through the post-hook:

fresh database205 MB database
git status (496 B)21.1 ms60.7 ms
cargo test (16.5 KB)24.5 ms64.5 ms

Payload size barely matters. Database size does, and that is the number to watch as your archive grows.

The distillation itself is single-digit milliseconds. Almost all of the rest is the archive write. Earlier releases measured 82 ms and 276 ms on the same machine, and the difference was three fixes rather than faster hardware: a tokenizer loaded per command for a reporting column, 249 line-filter regexes compiled whether or not their filter matched, and a connection pool opening four SQLite handles in a process that exits after one payload.

Measure latency by removal, not by a unit-test timer. A microbenchmark in the suite reported 66 ms for work that an A/B on the release binary put at 34.3 ms. Only the second kind of number is quotable.

Memory

Flat. The pipeline works on streams, so a 20,000 line log does not cost more resident memory than a short one.

Disk

One SQLite file at ~/.omni/omni.db.

Archived content is capped at 64 KB per block. That cap came from a measurement: archiving every lossy distillation cost 83.1 MB over 30 days, and the cap brought it to 13.3 MB while still covering 3,604 of 3,657 rows.

Benchmark traces are pruned at seven days (OMNI_TRACE_RETENTION_DAYS). That prune is why no published figure can be re-derived a week after it was measured.

Tokens

The thing you came for, and the honest version has two halves.

What it saves. Over 6,656 real commands on 0.7.3: 14.9% fewer bytes across the whole mix. By class, the spread is enormous:

classfilterswith the ledger
build and test76.9%78.0%
file reads0.0%25.0%
git, gh4.4%22.1%
search4.8%13.3%
infra4.4%8.2%
everything else0.6%6.9%

What it costs. Every marker is bytes the agent pays for, and 97.3% of calls save nothing while still paying the pipeline’s latency. On short output the marker can exceed the saving outright.

There is also a cost no byte count can express: a retrieval. When the agent needs content behind a handle, it pays a round trip it would not have paid if the bytes had simply arrived. Project-scope folds carry three times the profitability bar for exactly that reason.

The cost that is not OMNI’s to pay

On a flat-rate plan, compression does not reduce a bill at all. What it buys is session lifetime and fewer re-runs. Prompt-cache reads bill about a tenth of fresh input, so bytes saved once are not dollars saved per turn.

This is why the project’s own primary measure is context-window pressure for the same job, and reduction percentage is a diagnostic rather than a headline. See Where OMNI is going.

If it panics

It fails open. The raw output passes through and your agent never sees an error. Every hook runs inside catch_unwind, and a database that will not open costs session context rather than the whole pipeline.

Install

Get the binary

macOS and Linux, via Homebrew:

brew install fajarhide/tap/omni

macOS, Linux, WSL:

curl -fsSL omni.weekndlabs.com/install | bash

Windows, PowerShell:

irm omni.weekndlabs.com/install.ps1 | iex

From source, which needs the toolchain pinned in rust-toolchain.toml:

git clone https://github.com/fajarhide/omni
cd omni
cargo build --release

From inside Claude Code, if you would rather have the agent do the rest:

/plugin marketplace add fajarhide/omni
/plugin install omni@omni

On any agent that reads skills, the same skill installs through the skills directory CLI, and is listed at skills.sh/fajarhide/skills/omni:

npx skills add fajarhide/skills --skill omni

Either way that installs a skill, not the binary. The skill carries the install commands below, the verification step, and how to read the markers, so the agent stops guessing at any of the three. Everything on this page still applies; the plugin only means someone else types it.

Wire it into your agent

omni init            # the host you are running in, or a menu if you have a terminal
omni init --claude   # or --cursor, --codex, --gemini, and 11 more
omni init --all      # every host, and a .vscode/mcp.json in the current directory

omni init writes hooks and registers the MCP server. It is idempotent, so running it again after an upgrade is the right move rather than a risk.

With no terminal to prompt on, which is how an agent runs it, omni init configures the host it is running inside instead of failing on the absent menu. It says which host it picked. If it cannot name the host, a plain shell for instance, it stops and asks for a flag rather than installing into somewhere nobody asked for.

Every supported flag is in init. Which hosts get what is in Supported agents, and that page matters more than it sounds: a host that cannot rewrite its own shell tool’s output will not show the agent distilled bytes however well the pipeline works.

Verify

omni doctor

This is not optional ceremony. It checks the binary is on PATH, the database opens, the hooks are actually installed where the host reads them, and the MCP server is registered. omni doctor --fix repairs what it can.

Codex CLI needs one extra step. It runs only hooks it has been told to trust and skips the rest silently. After omni init --codex, start codex once and approve them under “Hooks need review”. omni doctor will keep failing until you do.

Confirm it is really running

omni doctor says the wiring is correct. This says the wiring is being used:

cat some-long-file.txt     # through your agent, not this shell
omni diff                  # raw against distilled, for the last command
omni stats

If omni stats shows rows and omni diff shows a difference, the hook is live.

A trap worth knowing now rather than later: the numbers in omni stats are split by agent_id, and a row recorded under terminal is TTY output no model ever read. When you are judging whether OMNI is earning its place, look at the rows for your actual host.

Upgrade

omni update      # Homebrew installs
brew upgrade omni

Re-run omni init afterwards if a release changes the hook contract. The changelog says when that happens.

Remove it

omni init --uninstall   # hooks and MCP registration for one host
omni reset --all        # every integration, and offers to wipe omni.db

omni reset without flags gives an interactive menu. Neither command touches your shell configuration, because OMNI never wrote any.

Your first hour

Assumes omni init and omni doctor are done. Nothing here changes configuration.

What the hour buys is the ability to check OMNI instead of trusting it. By the end you will be able to see any cut side by side with the original, pull back anything it removed, and tell one of its markers apart from a line that merely looks like one. That last skill is the one that makes the other two worth having.

See a distillation happen

Ask your agent to run something noisy. A test suite or a build is ideal.

Then, in your own shell:

omni diff

Raw on one side, distilled on the other, for the last command. This is the fastest way to develop either trust or suspicion, and both are useful.

Try one by hand

omni exec cargo test

omni exec runs a command through the whole pipeline and prints the result with a footer. It is the harness every bug report in this project is asked to use, because it takes the host out of the picture.

The argument form is exact: omni exec cargo test, not omni exec -- cargo test and not a quoted string. Both of those fail with “No such file or directory”.

Look at the numbers

omni stats

It leads with session lifetime, how many commands a session carries before the host closes it, because that is what the context window actually costs you. The distillation percentage below it is a diagnostic for one host’s pipeline.

Every absolute figure it prints is in bytes, which are counted. It used to report tokens, and those were the same byte counts divided by a constant calibrated against another vendor’s tokenizer, so the unit could not be defended even though the arithmetic was fine. Percentages were never affected: the divisor cancels in a ratio.

omni stats --detail        # per command, per route, per session, per agent
omni stats --rerun         # which distillers cost a re-run
omni dashboard             # the same numbers in a browser, on 127.0.0.1 only

--rerun is the interesting one. Reduction percentage cannot tell you whether a distiller removed something the agent then had to go and fetch again; this can.

Pull back something it removed

Every marker names a handle. Run it:

omni retrieve 0000000000000000

That exact handle is the documentation example and is refused by name, which is the point of this section. Copy a real one out of a marker in your own output and you get the bytes back verbatim, and the exit code tells you which happened: 0 when the handle resolved, 1 when it did not.

That pair is the fastest trust check there is. A tool that removes things and cannot give them back is a tool you have to take on faith.

Tell a real marker from one that is just text

This page is full of markers, so is OMNI’s own source, and so is any bug report that quotes one. Searching your transcript for the marker shape will find all of them.

The handle is what separates them. Worked examples everywhere in this manual use the reserved 0000000000000000, which no real fold can ever be assigned, so:

omni retrieve <handle-from-your-output>   # exit 0, and the content
omni retrieve 0000000000000000            # exit 1, "the documentation example"

If you are measuring whether OMNI did anything at all on a run, that exit code is the answer and grepping for [OMNI is not.

Pin what you are working on

omni goal set 'Migrate the billing service off the legacy queue'

The scorer favours output related to that goal, and the agent is reminded of it rather than drifting. omni goal show to check, omni goal clear to drop it.

Turn it off for one command

OMNI_PASSTHROUGH=1 kubectl get pods -o yaml

The first thing to reach for when you suspect OMNI changed something it should not have. If the output is identical with and without it, OMNI was not involved.

Things worth knowing before they bite

Reading a file through your shell may arrive distilled. Since the hook really does rewrite Bash output, a cat or sed of a source file can come back folded. Use your agent’s file-reading tool, or OMNI_PASSTHROUGH=1, when you need exact bytes.

A matched command may be rewritten before it runs. The pre-hook turns some commands into omni exec, redirection included, so the log file you later read is the distilled one. Break the prefix (env cargo test, or true && cargo test) when you need the raw log on disk.

Do not judge OMNI by output you read through OMNI. A cargo test read through the hook once reported “1 failed” for a 398-pass green suite. Redirect to a file with passthrough on before making any claim about a result.

When to ask for help

If output ever looks shorter than it should, if a row is missing, or if OMNI reports a success for something that failed, that is worth reporting. Reproduce it with omni exec first, and read the whole distilled output rather than a grep of it: grepping hides the headers that often make the output lossless after all.

Reading the markers

A marker is OMNI telling you what it did. There are only a few shapes, and knowing them is the difference between trusting the tool and suspecting it.

The shapes

[OMNI: 406 lines omitted, omni retrieve 0000000000000000 for full output]

Content was cut and archived. The 16 characters are a handle: omni retrieve <handle> prints the original back, byte for byte, from any shell in any session.

[OMNI: 40 lines already shown, omni retrieve 0000000000000000]

The ledger. These lines were emitted earlier in this session, so the claim is that the agent is still holding them and the handle costs nothing unless it wants to re-read.

[OMNI: 40 lines not shown here, omni retrieve 0000000000000000]

Also the ledger, different claim. These lines went to a different session of this project, and this agent has never seen them. The wording is deliberately not “already shown”, because that would be false. Folding them is a bet that the agent will not need them, and it carries three times the profitability bar for that reason.

That other session may also have been a different agent. The project history is keyed on the directory, so anything running in this repository contributes to it. See what two agents share.

[OMNI: identical to the 40 lines already shown, omni retrieve 0000000000000000]
[OMNI: identical to 40 lines from an earlier session, none shown here, omni retrieve 0000000000000000]

The same two claims, for a reply that is repeated in full. When the fold covers every line, the marker is the whole output rather than a gap inside it, so it says identical to and you get one line where a re-run would have printed the same hundreds. Anything less than the whole reply keeps the wording above.

[N similar lines collapsed]

Collapse. A run of near-identical lines, replaced by a count.

[OMNI Active] ⏺ 93.7% reduction (2.3 KB → 147 B) 3ms

The footer, on omni exec and pipe mode. Input size, output size, and how long the pipeline took.

[Partial signal]

The pipeline recognised some of the output but not all of it.

Reading a percentage correctly

The worst bugs in this project’s history reported the highest reductions. A distiller that deletes the answer compresses beautifully.

So a large number is not on its own good news. omni diff is the check:

omni diff     # the last command, raw against distilled

If a 99% saving turns out to have removed the file paths that were the answer, that is a bug worth reporting, and it is the exact class this project cares about most.

When there is no marker at all

Most of the time. Around 97% of calls save nothing and hand the output straight back. That is the pipeline working, not failing. It happens when:

  • The payload is JSON, YAML, CSV or TSV. Never touched, on purpose.
  • The command failed. A non-zero exit passes through verbatim.
  • There was no noise to remove. A kubectl get pods table is an enumeration where every row is a datum.
  • The output was too short to be worth a marker.

Getting content back

omni retrieve <handle>

Works on every host, with or without MCP. Agents with the MCP server wired can call omni_retrieve themselves without asking you.

One boundary a handle cannot promise: the archive is a rolling 30 day window, so omni retrieve on content older than that will not resolve. Verbatim traces are shorter still at seven days.

Telling a real marker from a printed one

Markers appear in prose too. This page is full of them, so is OMNI’s source, and so is any bug report that quotes one. That matters if you are measuring whether OMNI was active on a run, because searching a transcript for the marker shape will find the examples as readily as the folds.

The handle is what separates them. Every worked example in this manual and in OMNI’s own source uses one reserved value, 0000000000000000, which no real fold can ever be assigned:

omni retrieve 0000000000000000   # exit 1, "the documentation example"
omni retrieve <handle-you-found> # exit 0 if OMNI really folded it

So the exit code answers the question, and a marker copied out of documentation cannot be mistaken for evidence that anything was shortened.

Seeing what it saved

omni stats

Everything on this page reads the same aggregation, so a figure in the share card cannot drift from the one in the report.

The report

omni stats                 # last 30 days, the default
omni stats --today         # or --hour, --week, --month
omni stats --detail        # commands, routes, sessions, agents
omni stats --all-commands  # every command, not just the top ones
omni stats --project       # broken down per project path
omni stats --json          # machine readable

It leads with session lifetime: how many commands a session carries before the host closes it. That is the meter a user actually watches. The distillation percentage below it is a diagnostic for one host’s pipeline, not a product claim.

What the numbers are counted in

Bytes, and they are counted rather than derived. Every absolute figure the report prints is a byte total out of distillations, and every percentage is a ratio of two of them.

They used to be tokens, which were those same byte counts divided by 3.6, a constant calibrated against cl100k_base. That is GPT’s encoding, so the unit could not be defended even though the arithmetic was sound. Percentages were never affected: the divisor cancels in a ratio, which is why the reduction figures did not move when the absolute ones did.

One block is still an estimate and says so. The context breakdown accumulates file sizes from metadata, so Context Breakdown is exact for what it counts and is not a token count in disguise.

If you parse --json, the commands[].tokens_saved field is now bytes_saved. It held bytes under the old name for one release, which is a machine-readable surface asserting the wrong unit, so it was renamed rather than left lying. Consumers have to follow.

Reading it without fooling yourself

Split by agent_id before quoting anything. Rows recorded under terminal are TTY bytes no model ever read. On one installation those were 73% of every byte OMNI claimed to have saved. omni stats excludes them now, but the same trap waits for anyone querying the database directly.

A high percentage is not automatically good. The worst defects in this project’s history reported the highest reductions, because deleting the answer compresses very well. Pair any number with omni diff on a real command.

A low percentage is usually correct. Around 97% of calls save nothing because there was nothing to save. Structured payloads, failed commands and enumerations all pass through by design.

The check a percentage cannot make

omni stats --rerun

Which distillers cost a re-run. If a distiller removes something the agent then has to go and fetch again, the reduction was not a saving, it was a deferral. Nothing in a byte count can see that.

Sharing it

omni stats --share     # copy-pasteable summary of your own measured savings
omni stats --card      # the same summary written as an image

Both come from your own database, which is the point. A ratio claim in someone else’s README cannot be verified before installing.

In a browser

omni dashboard             # http://127.0.0.1:7717
omni dashboard --port 8080

Read-only, same database, binds loopback and nothing else.

Digging further

omni stats --detail              # per-command and per-route breakdown
omni query errors in last 5 commands
omni query warnings from cargo
omni query timeline today
omni patterns                    # errors that keep coming back
omni patterns --tool cargo

omni_history gives the same per-call rows to an MCP client. There is no omni history subcommand; this page listed one until 0.7.4.

omni query speaks a small fixed query language rather than free text. The supported forms are listed in its own help.

Querying the database directly

~/.omni/omni.db is plain SQLite and there is nothing stopping you.

Never read sqlite3 output through the Bash hook while investigating OMNI. The pipeline can fold the rows you are trying to count, and a LIKE filter that catches the wrong rows has already put a wrong figure into a published issue. List the rows before quoting any aggregate over them, and set OMNI_PASSTHROUGH=1.

Memory across sessions

The same agent that reads too much also forgets everything the moment you restart it. OMNI carries three kinds of memory, and they are kept for different lengths of time on purpose.

What is kept, and for how long

tierwhatkept
Permanentproject knowledge, recurring error patterns, engrams, goal memoryuntil you delete it, except goal memory which honours its own ttl_days
Working, 30 dayssessions, distillation rows, hot files, the archive, the event index, the ledgerrolling window
Verbatim, 7 daysexecution traces and the session transcriptshorter on purpose, two orders of magnitude heavier per row

The short answer to “will OMNI still know my project after a month away” is yes for the conclusions and no for the raw bytes. The boundary that matters in practice: omni retrieve on content archived more than 30 days ago will not resolve.

The ledger has one more way of forgetting that is not on a clock. At compaction its session half is dropped entirely, because compaction is where the agent stops holding what it was shown, and every “already shown” claim becomes false at the same moment. If folding seems to stop after a long session compacts, that is this, working. The project half survives, and The ledger explains the split.

Pinning a goal

omni goal set 'Migrate the billing service off the legacy queue'
omni goal show
omni goal clear

The scorer favours output related to the goal, and the agent is reminded of it on every prompt rather than drifting off task over a long session.

Facts worth keeping

omni remember 'The staging database ignores migrations run outside the deploy job'

Agents with MCP wired call omni_remember themselves, and pull facts back with omni_recall, which is a semantic search across engrams, stored knowledge and distillation history.

Store what is not derivable from the code: a decision and its reason, a gotcha, a constraint that no file states. Do not store what the repository already records.

Carrying a session across a restart

Session context is injected at session start, so a new agent knows which files were hot and what the last active error was. If the host closes or you switch tools, the project context is still there.

omni session --status
omni session --history
omni session --resume        # resume an interrupted session
omni session --transcript
omni session --health

For moving to a machine or a host that shares no database, omni_handoff exports the current session state as portable markdown you can paste into a new session. It is an MCP tool only; the CLI subcommand was removed. It is outside the set advertised by default, so set OMNI_MCP_TOOLS=all before reaching for it.

Engrams

Digests of finished subtasks, written as work completes rather than reconstructed later.

omni engram
omni engram --json

Knowledge that outlives a session

omni query errors in last 5 commands
omni patterns                # errors that keep coming back across sessions

omni_insight ranks the same recurring issues project-wide, and is an MCP tool with no CLI equivalent, and it is outside the default advertised set, so it needs OMNI_MCP_TOOLS=all. It was listed in the block above as though you could run it.

What it cannot do

It is per machine. There is no sync, no server, and no shared store between people. ~/.omni/omni.db is the whole of it, and a remote archive was explicitly not built rather than merely not built yet.

When something looks wrong

Work down this page in order. The first three sections rule out the look-alikes, which is where most suspicions end.

First, is OMNI even involved

OMNI_PASSTHROUGH=1 <the command>

Identical output with and without it means OMNI did nothing. That is the end of the investigation, and it settles more cases than anything else here.

Then check which path ran, because they are not the same:

omni --version && ls -la "$(which omni)"   # the installed binary, not your checkout
omni doctor

A closed issue still bites if the fix is unreleased.

Things that look like a bug and are not

Structured payload untouched. JSON, YAML, CSV, TSV, base64, terraform plans and anything destined for jq pass through by design. Not a missed opportunity.

Negative savings on small output, roughly -1% to -4%. The marker costs more than the compression saves on a short payload.

97% of calls saving nothing. Expected. There was nothing to save.

File reads showing zero token savings in a session that read many files. OMNI’s surface on most hosts is shell output. Your agent’s own file-reading tool, skill files and the system prompt are outside it.

kubectl binary streams corrupting. SPDY does that with or without OMNI.

Shell word splitting and quoting. That is your shell.

The traps that produce false conclusions

Do not judge OMNI by output you read through OMNI. A cargo test read through the hook once reported “1 failed” for a suite cargo itself called 398 passed. Redirect to a file with OMNI_PASSTHROUGH=1 before making any claim about a result.

Do not grep the distilled output. Grepping hides the group headers that often make output lossless after all. A 116 line search result looked like it had dropped every filename until the full payload showed a filename header per group with matches indented under it. Read the whole thing.

Output is not deterministic against a warm database. Session history feeds the scorer, so the same command can distill differently on two runs. Isolate it:

OMNI_DB_PATH=/tmp/probe.db omni exec <command>

A failed reproduction is not a verdict. If a bug does not reproduce, read the dispatch path in the source before concluding anything. A pipe that appeared to be discarded turned out to be the pre-hook wrapping the entire command string, so distillation landed upstream of the caller’s tail. Three hand-built reproductions had come back clean.

Common problems

The hook is installed but nothing is distilled. omni doctor checks the wiring. Then check the host’s tier: a Handoff-first or MCP-only host cannot rewrite its built-in shell tool’s output at all. See Supported agents.

Codex CLI does nothing after omni init --codex. It runs only hooks it has been told to trust and skips the rest silently. Start codex once and approve them under “Hooks need review”.

Warnings in the terminal that the agent never mentions. Hook rejections are recorded by the host as attachments that never enter the model’s context. The agent can genuinely believe the hook is fine while your screen fills with warnings. On Claude Code:

grep -c hook_error_during_execution ~/.claude/projects/<project>/<session>.jsonl

The attachment carries the host’s verbatim reason.

Commands feel slow. Expected, and it grows with database size rather than payload size: about 21 ms against a fresh database and 61 ms against a 205 MB one.

omni exec appears to hang. A warm shared database serialises writes. Give it its own with OMNI_DB_PATH.

Reporting it

Worth reporting, in this order of importance:

  1. A false claim. OMNI asserting a result its input does not support: a success reported for a failure, a count that does not match the runner’s own.
  2. Lost signal. Something needed was dropped without a marker saying so.
  3. Noise. Verbose but harmless.

A good report carries the raw output and the distilled output side by side, including the [OMNI Active] footer, the exact omni exec command, and omni --version. The footer is often the point: the worst bugs here report the highest reductions.

Reproduce on a synthetic command where you can, so there is nothing to redact. Real terminal output carries hostnames, account ids and internal addresses more often than people expect.

Tracker: https://github.com/fajarhide/omni/issues

Discord: https://discord.gg/zHTuvZhF2M, if you would rather ask before filing.

Commands

Every subcommand, grouped the way omni --help groups them: by what you are trying to do, not alphabetically.

omni <COMMAND> [FLAGS]
cmd | omni                # distill any command's output through a pipe

Set up

commandwhat it does
initInstall OMNI into your agent, hooks and MCP
doctorCheck the install is healthy, and fix what is not
updateUpgrade to the latest release
resetUninstall cleanly, keeping a backup of your config

See what it saved

commandwhat it does
statsHow many tokens were cut, and from which commands
retrievePrint the content a marker archived, by its handle
dashboardThe same numbers in a browser, on 127.0.0.1
diffThe last command’s output, before against after
sessionWhat this session has spent, and on what

Tune it

commandwhat it does
execRun one command through OMNI, to see what it would do
querySearch past distillations
patternsErrors that keep coming back

Memory

commandwhat it does
rememberSave a fact for future sessions
engramDigests of finished subtasks
goalPin a north-star goal so scoring favours it
versionVersion and environment details

Hook entry points

Not for typing. These are what an agent host invokes, and they are documented in Hooks.

omni --pre-hook      omni --post-hook     omni --hook
omni --session-start omni --session-end   omni --pre-compact
omni --mcp

A note on how flags are parsed

A match on the first argument routes the subcommand and hands the module the raw env::args(), so every module parses its own flags and declares its own accepted set. cli::check_flags rejects anything outside that set, which is what stops omni stats --detial printing the default overview and exiting 0.

Per-command help is real and worth reading: omni <command> --help. Where this reference and the help disagree, this records what the source accepts.

omni init

Installs OMNI into an agent: writes the hook configuration where that host reads it, and registers the MCP server.

omni init              # interactive menu, or the current host when there is no terminal
omni init --claude
omni init --all

Idempotent. Running it again after an upgrade is the right move, not a risk.

With no flags

On a terminal, a menu. Without one, which is how an agent runs it, the menu cannot be drawn, so omni init configures the host it is running inside and prints which one that is. A host it cannot name from the environment, a plain shell included, gets an error listing the flags rather than a guess: installing into a host nobody asked for is the worse of the two failures.

Hosts

One flag per host. Each writes that host’s own configuration format in that host’s own location.

flaghost
--claudeClaude Code (Anthropic)
--cursorCursor
--zedZed
--clineCline
--roo, --roo-codeRoo Code
--copilotGitHub Copilot CLI
--geminiGemini CLI
--opencodeOpenCode
--codexCodex CLI
--openclawOpenClaw
--antigravityAntigravity IDE, and generic webhook
--hermesHermes Agent
--vscodeVS Code (MCP)
--piPi Agent

What each host actually lets OMNI do differs a great deal. See Supported agents before assuming a flag buys shell distillation.

Modes

flageffect
--allEvery host above. Also writes .vscode/mcp.json in the current directory.
--hookHooks only, no MCP registration
--mcpMCP registration only, no hooks
--statusReport what is currently installed, change nothing
--uninstallRemove OMNI’s hooks and MCP server
--help, -hHelp

After running it

omni doctor

Always. init reports what it wrote; doctor reports whether the host is reading it.

Codex CLI needs one more step. It runs only hooks it has been told to trust and skips the rest without a word. Start codex once and approve them under “Hooks need review”. omni doctor fails until you do.

Notes

--all is the only flag that writes into the current directory. Everything else touches your home configuration only.

An unrecognised host flag does not always fail loudly: a misspelled one has been known to run the interactive default and exit 0 while installing nothing that was asked for. Read what it printed.

omni doctor

Checks that the installation is healthy, and repairs what it can.

omni doctor
omni doctor --fix

It covers the binary’s version and accessibility, the configuration directory and database, hook installation per host, MCP server registration, and signal loading.

Flags

flageffect
--fixRepair configuration and integration issues automatically
--detailPrint every integration row, not only the ones needing attention
--jsonMachine readable
--help, -hHelp

Reading the output

Host tiers. doctor prints the tier for every installed host, and the tier is the honest ceiling on what OMNI can do there. A Handoff-first or MCP-only host cannot rewrite its built-in shell tool’s output, so no amount of pipeline work will move its distillation numbers. See Supported agents.

[N UNRELEASED]. A build compiled from a tree whose CHANGELOG.md has entries under ## [Unreleased] says so, and tells you to cut a tag. On a release build there is no such line. This exists so a binary that was tagged without moving the changelog entries accuses itself rather than shipping quietly.

Live retention counts. How much is in each memory tier right now.

What it does not check

That the host is actually applying the rewrite. doctor verifies the configuration is where the host reads it, which is not the same as the host honouring it. The proof for that is a distillation row in the database under your host’s agent_id, or the host’s own session transcript.

On Claude Code, a hook payload the host rejected is recorded as an attachment that never reaches the model, so the agent can believe everything is fine while your terminal fills with warnings:

grep -c hook_error_during_execution ~/.claude/projects/<project>/<session>.jsonl

omni stats

Token savings analytics, read from your own database.

omni stats

Leads with session lifetime, how many commands a session carries before the host closes it. The distillation percentage below it is a diagnostic for one host’s pipeline, not a product claim.

Flags

flageffect
--detailFull breakdown: commands, routes, sessions, agents
--hour, -HScope to the last 60 minutes
--day, --today, -dToday only
--week, -wLast 7 days
--month, -mLast 30 days, the default
--all-commandsEvery command, not just the top ones
--projectBreak down per project path
--contextContext composition signals
--rerunWhich distillers cost a re-run
--shareA copy-pasteable summary of your measured savings
--cardWrite that summary as an image, sized for social posts
--jsonMachine readable
--help, -hHelp

--rerun is the one to know

Reduction percentage cannot tell you whether a distiller removed something the agent then had to fetch again. If it did, the reduction was a deferral, not a saving. This flag is the check that percentage cannot make.

Traps

Terminal rows are not tokens. Output written to a TTY is read by a human, not a model. On one installation those rows were 73% of every byte OMNI claimed to have saved. stats excludes them now, and so does the benchmark harness, but anyone querying ~/.omni/omni.db directly has to filter by agent_id themselves.

A high number deserves suspicion. The worst defects in this project reported the highest reductions, because deleting the answer compresses very well. Pair any figure with omni diff on a real command.

A low number is usually right. Around 97% of calls save nothing because there was nothing to save.

--share and --card cannot drift from the report. Both read the same aggregation as omni stats itself, which was a deliberate choice after an earlier version computed them separately.

omni exec

Runs one command through the full pipeline and prints the result, with a footer showing what it cost.

omni exec cargo test
cargo test: 411 passed, 1 failed
  FAILED ledger::tests::renders_identical_bytes_for_identical_state
[OMNI Active] ⏺ 93.7% reduction (2.3 KB → 147 B) 3ms

This is the harness every bug report in this project is asked to use, because it takes the host out of the picture. If a corruption survives omni exec, it is OMNI.

The argument form is exact

omni exec cargo test          # correct
omni exec -- cargo test       # fails: No such file or directory
omni exec 'cargo test'        # works, single-string form
omni exec sh -c 'a; b'        # works, split-argv form

The -- form is the one people reach for and the one that does not work.

Flags

flageffect
--session <id>Forward a host session id, which is what scopes the ledger
--agent <id>Record the run under a given agent_id
--help, -hHelp

Both are what the pre-hook uses when it rewrites a command into omni exec.

--session is worth knowing when you are investigating ledger behaviour: it is the only way to drive two distinct sessions by hand and see the difference between an already shown fold and a not shown here fold.

Isolate the database while probing

Output is not deterministic against a warm database, because session history feeds the scorer. Give each probe its own:

OMNI_DB_PATH=/tmp/probe.db omni exec <command>

A warm shared database also serialises writes, which is the usual reason omni exec appears to hang.

omni diff shows the same before and after for the last command the hook processed, which is what you want when the interesting command already ran.

omni retrieve

Prints the content a marker archived.

omni retrieve <handle>

The handle is the 16 characters inside a marker:

[OMNI: 406 lines omitted, omni retrieve 0000000000000000 for full output]
[OMNI: 40 lines already shown, omni retrieve 0000000000000000]

It returns the original bytes. Not a summary, not a re-run of your command, and not an approximation.

Works on every host, in any session, whether or not MCP is wired. Agents with the MCP server registered call omni_retrieve instead and never have to ask you.

What can go wrong

The handle does not resolve. The archive is a rolling 30 day window, so content older than that is gone. Verbatim execution traces are pruned sooner still, at seven days.

A handle that fails to resolve inside the window is a serious bug rather than an inconvenience, because a marker promising retrievable content is the one thing this mechanism cannot get wrong. Report it.

You typed the marker text, not the handle. Only the hex, no brackets, no prefix.

Why it can promise this

A run is archived before its marker is written, and a failed archive leaves the run verbatim rather than producing a marker. So a handle you can see is a handle whose content exists. That ordering was a fix, not the original design: an earlier version returned a key even when the write had failed.

omni session

Session state: what this session has spent, on what, and how to carry it across a restart.

omni session --status

Flags

flageffect
--statusCurrent session status
--historyRecent session history
--healthVisual session health dashboard
--transcriptTranscript of the recent session
--clearReset the current session
--continueContinue a stale session
--resumeResume an interrupted session
--injectEmit session context for an agent to consume
--jsonMachine readable
--help, -hHelp

omni sessions is accepted as an alias.

What a session is here

The scope key is the host’s session id, not an internal timestamp. That distinction was a real defect: an internal wall-clock id once covered 16 projects in one value, which would let the ledger tell one session it had been shown output that went to another.

That is also why omni exec takes --session: without a forwarded host id there is no ledger scope, and for a while the exec path therefore ran no ledger at all.

Continuity across a restart

Session context is injected at session start, so a new agent knows which files were hot and what the last active error was. Restarting your editor or switching hosts does not lose the project context.

--inject is the manual form of that, for a host wired to consume it.

For crossing to a machine that shares no database, use the omni_handoff MCP tool, which exports the state as portable markdown. The CLI subcommand of that name was removed; the MCP tool is unchanged. It is outside the default advertised set, so it needs OMNI_MCP_TOOLS=all.

Retention

Sessions are in the 30 day working tier. The verbatim transcript is in the 7 day tier, because it is two orders of magnitude heavier per row.

Everything else

The commands that need a paragraph rather than a page.

update

omni update

Fetches the latest release from GitHub and upgrades. Homebrew installations only at present; other install methods upgrade through their own channel.

Re-run omni init afterwards if the release notes say the hook contract changed.

reset

omni reset            # interactive menu
omni reset --all      # every integration, and offers to wipe omni.db
omni reset --claude   # one host

Per-host flags mirror init: --claude, --cursor, --zed, --cline, --roo / --roo-code, --copilot, --gemini, --opencode, --codex, --antigravity, --hermes, --pi.

--all is the only one that offers to delete your database, and it asks first. It keeps a backup of the configuration it removes.

dashboard

omni dashboard
omni dashboard --port 8080     # default 7717

The same numbers omni stats prints, in a browser. Read-only, reads the same database, and binds 127.0.0.1 and nothing else. Ctrl-C stops it.

diff

omni diff

The last command’s output, raw against distilled. The fastest way to build trust in what OMNI is doing, and the first thing to run when a result looks wrong.

query

omni query errors in last 5 commands
omni query warnings from cargo
omni query context for src/main.rs
omni query timeline today
omni query timeline today --json

A small fixed query language over distillation history, not free text. Four forms are supported and they are the four above. --json for machine-readable output.

patterns

omni patterns
omni patterns --tool cargo

Errors that keep coming back across sessions. --tool <name> scopes to one tool.

Useful for the question “have I hit this before”, which is the one a fresh session cannot answer on its own.

remember

omni remember 'The staging database ignores migrations run outside the deploy job'

Stores a fact in persistent memory, retrievable later through omni_recall or the session context injection.

Worth storing: a decision and its reason, a gotcha, a constraint no file states. Not worth storing: anything the repository already records.

engram

omni engram
omni engram --json

Digests of finished subtasks, written as work completes.

goal

omni goal set 'Migrate the billing service off the legacy queue'
omni goal show
omni goal clear

Pins a north-star goal. The scorer favours output related to it, and the agent is reminded of it rather than drifting over a long session. Goal memory honours its own ttl_days rather than the standard retention tiers.

set is the default subcommand, so omni goal 'some text' also works.

version

omni version
omni version --json

Version and environment details: build date, git hash, and the paths OMNI resolved for its configuration and database. Worth including in any bug report.

Environment variables

Every OMNI_* variable the binary reads. Grouped by why you would reach for one.

The one you will actually use

variableeffect
OMNI_PASSTHROUGH=1Skip the pipeline entirely. Raw output, every time.

This is the first thing to reach for when you suspect OMNI changed something it should not have, and the thing to set when you need exact bytes from a file read through your shell. Identical output with and without it means OMNI was not involved.

Where things live

variableeffect
OMNI_HOMEPuts the whole tree, config and data, in one directory
OMNI_CONFIG_HOMEConfig directory, when you want it split from data
OMNI_DATA_HOMEData directory, likewise
OMNI_DB_PATHPath to the SQLite database
OMNI_TRANSCRIPT_DIRWhere session transcripts are written

OMNI_DB_PATH earns its own note. Point it at a scratch file whenever you are probing OMNI’s behaviour by hand:

OMNI_DB_PATH=/tmp/probe.db omni exec <command>

Output is not deterministic against a warm database, because session history feeds the scorer, and a shared warm database serialises writes, which is the usual reason omni exec looks like it has hung. It is also required when running the test suite against a live installation.

Commands run through the MCP server

variableeffect
OMNI_RUN_TIMEOUT_SECSHow long omni_run waits for a command. Default 60.

The default sits below every host MCP timeout we know of, so a stalled command comes back as a sentence naming itself rather than the host’s idle-timeout error. Raise it when a build legitimately takes longer, and remember the host has a deadline of its own: Cursor’s is 120 seconds, and nothing OMNI does can extend it.

Retention

variableeffect
OMNI_TRACE_RETENTION_DAYSDays of verbatim execution traces. Default 7.
OMNI_SESSION_TTLSession time to live, in minutes

Hold the trace window open while a measurement is in flight:

OMNI_TRACE_RETENTION_DAYS=90 ...

Seven days is why no published benchmark figure can be re-derived a week after it was measured, including by the people who published it. Raise it before you start, not after.

Context pressure

variableeffect
OMNI_CONTEXT_WINDOWContext window size hint, in tokens
OMNI_PRESSURE_WARNWarning threshold, as a share of the window
OMNI_PRESSURE_CRITICALCritical threshold

OMNI estimates how full the session’s context is and injects a warning past these thresholds. Set the window to match the model you are actually running.

Session behaviour

variableeffect
OMNI_FRESHForce a fresh session rather than continuing one
OMNI_CONTINUESet internally by the dispatcher to mark a continued session
OMNI_SUBAGENT=1Sub-agent mode
OMNI_AGENT_IDAgent identity, recorded on every row

OMNI_AGENT_ID is the one to understand before quoting any number. Every distillation row carries it, and rows recorded under terminal are TTY bytes no model ever read. Blending those with hook rows once made 73% of a published saving fictional. When several agents run side by side, give each its own id.

Loops

variableeffect
OMNI_LOOP_IDLoop identifier. Alphanumeric and dash, 64 characters.
OMNI_LOOP_GOALGoal string, 500 characters, no shell metacharacters
OMNI_LOOP_BUDGETToken budget per iteration, up to 10M
OMNI_LOOP_ITERATIONCurrent iteration number. Default 0.

See Loop engineering.

Output

variableeffect
OMNI_QUIET=1Suppress the stderr stats line in pipe mode
OMNI_OUTPUT_JSONJSON output from the pipe path
OMNI_EXPORT_CSVExport session data as CSV at session end

Build and internal

Not for setting by hand. Listed so that seeing one in a stack trace or a generated config is not a mystery.

variableset by
OMNI_BINWritten into the generated Hermes plugin, naming the binary path
OMNI_CMDThe command being processed, falling back to CMD
OMNI_GIT_HASH, OMNI_BUILD_DATEStamped at build time, reported by omni version
OMNI_UNRELEASED_ENTRIESComputed by build.rs from CHANGELOG.md, so a binary built from an untagged tree says so in omni doctor
OMNI_PI_PACKAGE_SOURCEPackage source for the Pi agent integration
OMNI_DATA_HOME_UNSET_FOR_TESTTest fixture only

Benchmarking

variableeffect
OMNI_BENCH_DBDatabase to replay from
OMNI_BENCH_ALL=1Replay the wider population including terminal output
OMNI_BENCH_RTKPath to an rtk binary, adding the head-to-head arm

OMNI_BENCH_ALL exists so the harness can say which population it measured rather than leaving it to be inferred. Including terminal output printed 79.1% where the model-facing population printed 43.3%, on the same data.

MCP tools

omni init registers OMNI as an MCP server, which gives the agent tools it can call itself without going through you. This page describes all 25. Your host is told about a subset.

What your host is told about

Tool definitions sit in the prefix of every request, so a tool nobody calls is re-read on every request of every session rather than paid for once. OMNI advertises the set your host’s tier can use:

tieradvertised
Full, and any host OMNI does not recognisethe nine below
Handoff-firstthe same nine, with omni_run always among them
MCP-onlyomni_remember, omni_recall, omni_retrieve, omni_knowledge

The nine are omni_retrieve, omni_explain_savings, omni_remember, omni_recall, omni_run, omni_find_noise, omni_context_breakdown, omni_history and omni_context. They are the ones that were actually called across the recorded corpus.

Every other tool on this page is still in the binary and one setting away. OMNI_MCP_TOOLS=all advertises all 25, and omni doctor says which set is in force and which host it resolved:

  MCP tools:      9 of 25 advertised to claude_code (OMNI_MCP_TOOLS=all restores the rest)

Confirm the list against your own binary rather than this page:

{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"p","version":"1"}}}'
  echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; } \
| omni --mcp | tail -1 | jq -r '.result.tools[].name'

Getting content back

toolwhat it does
omni_retrieveRetrieve full content a marker omitted, by its handle
omni_runRun a shell command and return distilled output
omni_signal_extractExtract signal from raw text, without the hook pipeline

omni_run matters most on hosts that cannot rewrite their built-in shell tool. There, it is the only path to distilled output, which is why omni init --cursor installs a rule telling the agent to prefer it.

Understanding what OMNI did

toolwhat it does
omni_explain_savingsRoute, filter, input and output bytes, savings % per recent command
omni_historyRecent distillations with per-call savings and ratios
omni_context_breakdownToken breakdown by source for the current turn
omni_densityHow much signal against noise in a piece of text
omni_budgetToken budget usage and compression efficiency for this session

These four are the right answer to “is OMNI helping here”. Pull the numbers rather than forming an impression.

Memory

toolwhat it does
omni_rememberStore a decision, gotcha or constraint
omni_recallSemantic search across engrams, knowledge and distillation history
omni_knowledgeQuery or store cross-session project knowledge
omni_insightTop recurring issues and error patterns across the project
omni_adaptive_insightsRetrieval patterns, as a judgement on distillation effectiveness
omni_handoffExport session state as portable markdown, no network needed

omni_handoff is MCP only. The CLI subcommand of that name was removed.

toolwhat it does
omni_sessionSession state: status, context, clear
omni_searchSearch this session’s history
omni_queryQuery distillation history with the fixed query forms
omni_contextLightweight dependency context for a file
omni_agentsOther agents currently active on this project

Tuning

toolwhat it does
omni_find_noiseAnalyse recent raw traces for repetitive noise

Advisory only, and the learner treats “repeated” as “noise”. It has suggested stripping ^metadata:, ^spec:, code fences and ^\[stderr\], which are structure and the error channel. Never paste its output anywhere without reading it line by line.

Loops

toolwhat it does
omni_loop_statusOne-call status check for an orchestrator before each iteration
omni_loop_memoryRead and write loop memory that survives session restarts
omni_set_loop_contextUpdate loop context dynamically
omni_budget_statusBudget status for this iteration. Call before expensive work.
omni_verifyAs a checker sub-agent, evaluate the maker agent’s recent work

See Loop engineering.

A tool that is not one

omni_auto_noise appears as a string in the server source and is not a tool. It is a filter name passed to the TOML generator. Calling it returns -32602 tool not found.

It has been miscounted before: a source grep for "omni_*" returns 27, and 27 is therefore wrong wherever it appears. Run the tools/list call above for the count.

Hooks

The entry points an agent host invokes. You never type these; omni init writes them into the host’s configuration.

entry pointwhen the host calls it
omni --pre-hookBefore a tool runs
omni --post-hookAfter a tool produces output
omni --hookUniversal dispatcher, for hosts with one hook slot
omni --session-startSession begins
omni --session-endSession ends
omni --pre-compactBefore the host compacts the conversation
omni --mcpRun as an MCP server over stdio
cmd | omniPipe mode, no host involved

One call, both hooks

The shell runs whatever the pre-hook handed it and never knows OMNI exists. Only the reply is rewritten, which is why nothing here can change what your command did.

What each one does

Pre-tool decides whether a command should be routed through OMNI at all, and can rewrite it into omni exec. That rewrite wraps the entire command string, redirection included, which is why a matched command’s log file on disk can turn out to be the distilled version. Break the prefix (env cargo test) when you need the raw log.

Post-tool is the main event: the raw output arrives, the pipeline runs, and the distilled result is handed back for the host to substitute.

Post-tool-failure exists because a failed command must pass through verbatim, and hosts disagree wildly about how they say a command failed. Claude Code sends a plain string, Error: Exit code N. Others carry structured error flags. Reading only one shape is a bug this project has had.

Session start injects project context: hot files, the last active error, stored knowledge, the pinned goal.

Session end writes the summary and can export CSV.

Pre-compact is the host’s warning that the conversation is about to be shortened.

Two doors into one pipeline

post_tool and pipe are separate entry points that run the same stages, and keeping them in step has been a recurring source of bugs. Three separate fixes each corrected one copy and left the other. The ledger stage existed in post_tool for a release before pipe had it at all, so a command the pre-hook rewrote into omni exec got the filters and nothing else.

If you are changing pipeline behaviour, change both, or check why not.

Why it never crashes your agent

Every hook runs inside catch_unwind, at the highest entry point. A panic in one stage costs that distillation, not the session. A database that will not open costs session context, not the pipeline.

That is the fail open rule, and it has one sharp edge worth stating: failing open means handing back the raw bytes. It does not mean emitting a cheerful summary. A distiller that parsed nothing returning 0 tests passed is failing closed, and confidently.

What a host has to do for any of this to matter

Register the hook, and then honour what it returns.

The second half is not guaranteed. OMNI once emitted its distilled output under a key Claude Code ignores, so nothing was applied on that path for two releases while OMNI recorded a saving and printed a footer for each one. The fix corrected the key and left the value shape wrong, and the symptom survived unnoticed.

Two things that taught, both non-obvious:

  • The rewrite is validated against the host tool’s own output schema, one shape per tool. There is no universal shape.
  • The fields are independent. A rejected rewrite still lets the context message through, so the savings footer prints for a distillation that was reverted.

So the proof that a hook is working is not the footer and it is not omni stats. It is the host’s own session transcript:

grep -c hook_error_during_execution ~/.claude/projects/<project>/<session>.jsonl

A warning you can see is not a warning the agent can see. Those attachments never enter the model’s context, so an agent can tell you the hook is fine while your terminal fills with rejections.

Testing a hook by hand

Feed it a payload directly rather than guessing which path ran:

echo '<host payload json>' | omni --post-hook

omni exec and the post-hook route differently, so a result from one is not evidence about the other.

The payload shape, which differs per tool

Getting this wrong fails silently and identically: the hook exits 0, prints nothing, and a probe reads that as 0.0% saved. There is no error to notice, so a distiller that is in fact cutting 96% can be written off as not firing.

Bash puts the output at the top of tool_response:

{ "session_id": "s1", "tool_name": "Bash",
  "tool_input":    { "command": "cat server.log" },
  "tool_response": { "content": "line one\nline two\n" } }

Read wraps it in file, and the extra keys are not decoration. startLine is what the host counts cat -n numbering from, so a fold that removes lines above the survivors has to move it:

{ "session_id": "s1", "tool_name": "Read",
  "tool_input":    { "path": "notes.txt" },
  "tool_response": { "file": { "filePath": "notes.txt", "content": "...",
                               "startLine": 1, "numLines": 40, "totalLines": 400 } } }

The reply goes under hookSpecificOutput.updatedToolOutput, and Claude Code validates it against the host tool’s own output schema. A wrapped Read gets a file reply back, which is the shape the host accepts.

A bare tool_response.content does not get a bare reply. Verified rather than assumed: it comes back as {status, result}, which is OMNI’s own shape and is what #187 was about. So the bare form is fine for asking what the ledger did, and its reply is not what a real Read would accept.

Both Read shapes are real and they reach different stages. A Read payload written with a bare tool_response.content is accepted and reaches the ledger, while tool_response.file.content reaches the readfile distiller and the startLine adjustment. Neither is wrong; they answer different questions. A probe aimed at one and built on the other returns a clean nothing and looks like a verdict.

Supported agents

Which host you run decides what OMNI can do, and the ceiling is the host’s, not the pipeline’s. This page is worth reading before judging whether OMNI is earning its place.

The tiers

tierhostswhat you get
FullClaude Code, Codex CLI, Gemini CLI, Aider (pipe)The host applies OMNI’s rewrite, so the model reads distilled output from its own built-in tools.
Handoff-firstCursor, WindsurfThe host cannot rewrite built-in tool output. omni_run distils anything routed through it, and omni init --cursor installs the rule that makes the agent reach for it.
MCP-onlyCline, Roo, OpenCode, VS Code, Zed, Copilot, Antigravity, Hermes, PiMemory, recall and session state. No shell distillation, and no claim of it.
omni doctor     # prints the tier for every installed host

Savings are only ever counted where the model actually received less. A host that cannot apply the rewrite will not move the distillation numbers however good the filters get, and claiming otherwise would be the same defect as a distiller reporting a saving it did not make.

Installing for each

omni init --claude       omni init --cursor      omni init --zed
omni init --cline        omni init --roo         omni init --roo-code
omni init --copilot      omni init --gemini      omni init --opencode
omni init --codex        omni init --openclaw    omni init --antigravity
omni init --hermes       omni init --vscode      omni init --pi
omni init --all

Host-specific notes

Codex CLI runs only hooks it has been told to trust, and skips the rest without a word. After omni init --codex, start codex once and approve them under “Hooks need review”. omni doctor fails until you do. This has bitten before: Codex ran zero hooks for a whole release while everything looked correctly installed.

Cursor cannot rewrite its built-in shell tool’s output. Intercepting the shell by denying execution and returning output as a hook message is technically possible and was rejected: it tells the agent its command was blocked, loses the exit code, moves execution semantics into OMNI, and bypasses the host’s approval flow.

Claude Code matches more than Bash. The post-tool matcher is Bash|Read|Grep|WebFetch, which is what finally let the file-read, search and fetch distillers run at all. Three of them had been fully written and tested and had never executed on a real session.

Hermes has its own integration page: Hermes Agent.

Windows is supported. Paths, line endings and the .exe suffix are handled, and the CI matrix includes windows-latest.

Several agents at once

Give each its own identity so the numbers stay separable:

OMNI_AGENT_ID=claude ...
OMNI_AGENT_ID=cursor ...

omni_agents reports which agents are currently active on the project. Every distillation row carries the id, and any figure that blends them is describing a mix rather than a product.

Adding a host

The agent modules live in src/agents/, one file per host, and each writes that host’s own configuration format in that host’s own location. The pattern is small and mostly mechanical.

The part that is not mechanical is verification. “Provider unreachable” is not a reason to leave a hook path unverified: serve the API and fake only the model. Hooks that had never run in production have been found on three hosts by doing exactly that.

Architecture

Local, deterministic, and the same input always produces the same output. Nothing leaves the machine at any stage.

src/
├── main.rs              CLI dispatch, and the single command list
├── lib.rs               library re-exports, so the crate is testable
├── paths.rs             path resolution
├── agents/              one file per host: claude, cursor, codex, hermes, pi, …
├── cli/                 one file per subcommand
├── distillers/          12 content filters
├── graph/               code graph indexing
├── guard/               safety, limits, trust bounds, env hygiene
├── hooks/               the entry points, and the dispatcher that routes them
├── ledger/              cross-turn line dedup
├── mcp/                 the MCP server and its 25 tools
├── pipeline/            scorer, collapse, registry, format gate
├── session/             tracking, learning, adaptive thresholds
├── store/               SQLite and transcripts
└── util/                command families, token estimation

About 46,000 lines of Rust.

Design rules that the code actually enforces

Library first. main.rs is a thin entry point. Logic lives in lib.rs and its submodules, so OMNI can be tested as a crate.

Single source of truth. Command-to-behaviour mapping is centralised in pipeline/registry.rs. Duplicated matches!(cmd, ...) blocks in distillers and scorers are the thing that rule exists to stop. Magic numbers live as named constants in pipeline/mod.rs or guard/limits.rs.

IO separated from logic. Scoring and filtering are pure functions over &str. They do no filesystem or network work.

Panic safety. Every hook runs inside catch_unwind at the highest entry point, so one failing hook cannot take down the host agent.

Graceful degradation. If the database will not open, hooks still work, without session context.

Deterministic. No randomness anywhere. The ledger’s handle is a content address and carries no timestamp, because an earlier {timestamp}_{hash} form made 4 of 73 repeated inputs emit different bytes.

The database

One SQLite file, ~/.omni/omni.db.

tableholds
sessionssession state, task and domain hints
distillationsevery distillation: filter, bytes in and out, route, score, latency, agent
file_accesshot file tracking per session
rewind_storecompressed content by SHA-256, with a retrieval counter
session_eventsFTS5 full-text index
ledger_lineswhich lines a scope has been shown
ledger_foldsone row per marker issued: which scope, and which agent’s bytes it drew on
passthrough_eventstelemetry for commands that bypassed the pipeline
unhandled_toolstools OMNI does not support natively yet
execution_tracesraw input and distilled output per command
session_summariesper-session metrics
project_knowledgecross-session semantic memory
agent_sessionsshared state across multiple agents

passthrough_events and unhandled_tools are worth knowing about: they are how a coverage gap becomes visible instead of staying a guess.

Cross-platform

The CI matrix includes windows-latest, and four rules keep it green:

  1. No hardcoded separators. PathBuf and push, never / or \\.
  2. No exact \n matching in assertions. Use .lines(), or normalise \r\n first. The ledger splits with split_inclusive('\n') rather than lines() for exactly this reason: lines() drops the terminator, so rebuilding with \n would silently rewrite every CRLF payload on Windows.
  3. No assuming the binary is ./omni. Use std::env::consts::EXE_SUFFIX.
  4. Environment variables are case-insensitive on Windows. Use eq_ignore_ascii_case when reading std::env::vars().

Build

cargo build --release
cargo test --all
make ci

The toolchain is pinned in rust-toolchain.toml, currently 1.97.0, and the pin is load-bearing. A release once produced no binaries at all because release.yml asked for stable per cross target while the pin said otherwise, and every cross-compile died with can't find crate for core before compiling a line. ci.yml stayed green throughout, because it only builds host-native.

The pipeline, stage by stage

Read → Guard → Score → Distill → [Collapse] → Ledger → Route → Persist

The order is fixed. This page is about what each stage may and may not do, which is where the bugs live.

The brackets around Collapse are the part people get wrong, including this page until recently. It is a fallback, not a step.

Guard

pipeline::format::sniff classifies the payload. Some(Structured) ends the pipeline and the bytes pass through.

Four kinds: JSON, YAML, CSV, TSV. The bias is deliberate: bracketed but unparseable, truncated, or comment-bearing JSON all count as structured, because compression cannot repair a malformed payload and can certainly make it worse.

Above a size threshold, bracket shape alone decides JSON, since a full serde_json parse would blow the latency budget.

The YAML sniffer skips lines introduced by a block scalar indicator (key: |). One embedded ConfigMap once sank a 608-line kubectl kustomize manifest: the block’s contents carried no key:, so the sniff said “not YAML” and the manifest went down the lossy path.

Score

scorer::score_with_command(input, cmd, session) returns Vec<OutputSegment> with tiers: Critical 1.0, Important 0.7, Noise 0.1.

semantic::is_critical tiers the block before any distiller runs. When a distiller behaves oddly, probe the segment tiers first; the tier may already have decided the outcome, and a guard added to the distiller will not move it.

Pure function. No IO.

Collapse

Runs of near-identical lines become [N similar lines collapsed].

It runs after Distill, and only when Distill did not earn its keep. Both hooks score and distill the raw content, then ask beats_guardrail; only if that fails does the collapsed form get used instead. A distiller therefore sees the original text, never collapse markers.

This page said the opposite until 0.7.4, which was true before #116 and wrong for two releases after it. The behaviour is pinned by kubectl_table_distills_from_raw_not_collapse_markers: the bug it guards against is a column parser reading [30 similar lines collapsed] as a pod row.

The mode is picked by specificity. A kubectl … | grep payload exercises the Infra path rather than the Log path, so a fixture chosen to test a collapse guard can pass with the guard removed. Check which mode your fixture actually reaches.

Distill

registry::resolve_profile(command) picks the distiller, then:

fn distill(&self, segments: &[OutputSegment], input: &str,
           session: Option<&SessionState>) -> Option<String>;

Option, and that is the whole design. A distiller that parsed nothing returns None and the caller hands back the raw bytes. The invariant lives in the trait rather than in each author remembering to call a helper, so it holds for all 12 by construction.

The TOML layer that used to short-circuit this stage was retired in 0.7.4, so the Rust code is now the only thing that can claim a command.

The ledger

After distillation, ledger::Ledger replaces runs of lines the scope has already been shown with a handle. Gated on the same format sniff as collapse.

It is append-only, which is what keeps the upstream prompt cache intact: a cache works on a prefix, so shortening the suffix costs nothing while retroactive compaction would destroy it.

See The ledger for the two scopes and their different claims.

Persist

The raw input is archived by SHA-256 and the marker carries the handle.

Order matters and is not negotiable: archive, then write the marker. A failed archive leaves the run verbatim. Doing it the other way round produces, on any write failure, a marker pointing at content that was never stored.

Recording is unconditional even when the projection saved nothing, because a block is worth remembering in case it is seen again.

Two doors, one pipeline

hooks/post_tool.rs and hooks/pipe.rs both run these stages. Keeping them in step is a live maintenance problem: three separate fixes each corrected one copy and left the other, and the ledger stage existed in one for a release before the other had it at all.

Change both, or write down why not.

Adding a stage

Do not, unless the measurement says so. The pipeline earns its shape from a replay harness, and the useful pattern is to price a proposal before building it:

  • “Route a pipeline by its last stage” sounds obviously right, and would have handed 871 of 1,035 recorded pipelines to head, tail or sed, all verbatim passthroughs, stopping distillation on them entirely.
  • Quote-aware chain splitting keeps 205 of 2,928 routed commands, which is what justified 25 lines of scanner over a 5-line naive split.
  • An import-graph signal for the scorer sized at 196 traces, until the graph itself turned out to be wrong. Corrected, it sized at 26, against a 542 ms build on a 10 ms budget.

A measurement that kills a design is the measurement working.

Adding a distiller

The most common change here. Five steps, and the fourth is the one that matters.

1. The module

src/distillers/my_type.rs:

use crate::pipeline::{OutputSegment, SessionState};
use super::Distiller;

pub struct MyDistiller;

impl Distiller for MyDistiller {
    fn distill(
        &self,
        segments: &[OutputSegment],
        input: &str,
        session: Option<&SessionState>,
    ) -> Option<String> {
        // Return None the moment you are not sure you parsed this.
        todo!()
    }
}

Return None whenever parsing failed. That hands back the raw bytes, which is the correct answer and the one the whole design rests on.

Never return a success string from a zero state. vitest: ✓ 0/0 passed for output that was actually a dev server is failing closed, confidently, and it is the exact defect this project keeps fixing.

2. Register it

src/distillers/mod.rs:

pub mod my_type;

// in get_distiller():
ContentType::MyType => Box::new(my_type::MyDistiller),

Routing belongs in pipeline/registry.rs. Do not add a matches!(cmd, ...) block inside the distiller; that duplication is what the registry exists to prevent.

3. A realistic fixture

tests/fixtures/my_type_example.txt. Real output from the real tool, not something hand-written to be easy to parse.

4. A snapshot test, and prove it can fail

snapshot_test!(test_my_type_distillation, "my_type_example.txt", ContentType::MyType);
cargo test
cargo insta review

Then break the rule deliberately and watch the test go red, before restoring it. A check that cannot fail proves nothing, and this repo has shipped two regression tests that could not fail.

Two specific ways a test here passes for the wrong reason:

  • Your fixture reaches a different collapse mode than you think. A kubectl … | grep fixture exercises Infra, not Log, so a guard you are testing may never be consulted.
  • “No rewrite from the hook” is not proof the distiller punted. It can mean the format gate fired, or the guardrail rejected the result.

A distiller can also return a near-copy rather than the exact input, so detect “this did not help” with beats_guardrail rather than comparing against the input.

5. Gates

cargo fmt
cargo clippy -- -D warnings
OMNI_DB_PATH=/tmp/t.db cargo test

OMNI_DB_PATH is not optional. Parallel tests competing for ~/.omni/omni.db cause SQLite locks: 79 seconds green against an isolated database, 433 seconds and then a hang against the live one.

Before you write any of it

Measure the workload. ~/.omni/omni.db prices a proposal in one query, and the answer is often the opposite of the request.

“Improve the python3 distiller” turned into two facts in two queries: python3 was already reporting 97.2%, and the savings were the collapse fallback deleting data rows. The obvious feature, a traceback distiller, died on 9 of 7,506 traces containing a traceback.

-- distillations.filter_name is the command's first token
-- execution_traces holds raw_input and distilled_output in full

Read the rows before quoting an aggregate over them. A LIKE filter that caught the wrong rows has already put a wrong figure into a published issue.

And never read sqlite3 output through the Bash hook while doing this. The pipeline can fold the rows you are counting.

The bar the result has to clear

Not “did it compress”. These:

  1. Would the agent still have the answer?
  2. Does anything dropped leave a marker?
  3. Does the reported number describe what actually happened?

A patch that raises reduction percentage while removing signal is the project’s own recurring defect, shipped again with your name on it.

Testing

OMNI_DB_PATH=/tmp/omni-test.db cargo test

Start with that line. It is not a suggestion.

The two guardrails that waste the most time

Isolate the database. Parallel integration tests competing for ~/.omni/omni.db cause SQLite locks and hangs. Measured: 79 seconds green against an isolated database, 433 seconds and then a hang against the live one. tests/hook_e2e.rs has an omni_cmd() helper that spawns the binary with a unique OMNI_DB_PATH from a NamedTempFile. Use it.

Lock early, release fast. Rust mutexes are not reentrant, so nested or redundant lock() calls on session_arc deadlock. Open a scope, take what you need, let the guard drop before doing anything that might lock again.

If cargo test runs over a minute on macOS or Linux, suspect one of those two. Check pipe mode and the E2E tests first; they are the heaviest.

Suites

cargo test                              # everything
cargo test --test hook_e2e              # binary spawn, end to end
cargo test --test savings_assertions    # per-filter savings thresholds
cargo test --test security_tests
cargo test distillers::tests            # snapshots
cargo insta review                      # approve snapshot changes
tests/smoke_test.sh ./target/debug/omni

tests/fixtures/ holds 45 realistic tool outputs. Add real output from the real tool, not something shaped to be easy to parse.

Naming

Inside #[cfg(test)], drop the test_ prefix. The attribute already says it is a test.

fn returns_default_when_config_missing()
fn excludes_sensitive_data_from_summary()
fn preserves_errors_during_collapse()
fn renders_identical_bytes_for_identical_state()

Not test_config_ok, not handles_it, not valid_json. Start with a verb, say what the behaviour is, English only.

Design

One behavioural assertion per test. Arrange, act, assert, with the sections visible. Test observable behaviour, not internals: assert_eq!(result.status, Status::Ready) rather than assert!(internal_cache.len() > 0).

Every non-trivial feature carries a happy path, an edge case, a malformed input case, a regression case if it is a fix, and an explicit no-panic case. Malformed input must return Err, never panic.

Prove the test can fail

Break the rule deliberately, watch it go red, restore it.

This repo has shipped two regression tests that could not fail. Both looked correct. Both passed with the fix reverted.

Three specific ways a green test here means nothing:

Your fixture reaches a different code path than you think. Collapse mode is picked by specificity: a kubectl … | grep fixture exercises Infra, not Log, so a collapse-guard test passes with the guard removed.

“No rewrite from the hook” is not proof the distiller punted. It can equally mean the format gate fired or the guardrail rejected the output.

A distiller can return a near-copy rather than the exact input. Detect “this did not help” with beats_guardrail, not output == input.

Proving a refactor changed nothing

For behaviour-preserving work, diff distiller output over the whole recorded corpus, about 5,100 commands times 11 probes, then break one arm deliberately to show the harness has teeth. A differential harness that cannot detect a planted difference is not evidence.

Gates

make ci      # fmt + clippy + test + security + binary-check

Or individually:

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all

Zero clippy warnings. Not “few”.

source "$HOME/.cargo/env" first, so you get the pinned 1.97.0 rather than a Homebrew cargo that ignores rust-toolchain.toml and will keep drifting from CI.

Never weaken a check to make it pass

Not an assertion, not a security check, not a threshold. If a test is in the way, it is either wrong, in which case fix it and say why, or right, in which case the code is wrong.

Benchmarks

One developer’s real command history, replayed on 0.7.5. Every figure below comes from the same run, including the ones that do not flatter us.

Corpus: 5,984 traces, 23,086,649 bytes, 2026-08-11 11:03:00 to 2026-08-14 18:11:10 UTC, all agent_id='claude_code', 123 terminal rows excluded from 6,107, 0 errored. Replayed in 1,238 s.

The headline

32.6% fewer bytes from the filters. 69.6% with the ledger. 23,086,649 to 15,557,823 to 7,026,021.

tokens, filters only7,682,124 to 4,874,124, 36.6%
bytes per token3.005 raw, 3.192 distilled (the shipped estimate is 3.6)
calls that saved nothing96.1%, 5,748 of 5,984
calls that shrank3.9%, 236
calls that grew0
ledger folds882 calls, 3,231 session markers, 86 project markers
raw bytes already shown once68.4% before filters, 64.7% after
that repetition, by scope67.3% same session, 1.1% earlier session, same project

Filtering and repetition are orthogonal. That is the argument for the ledger, and on this corpus the ledger is worth more than twice what the filters are.

Read the corpus before the number. This window is unusual and it inflates everything below. 148 of the 5,984 calls carry 64.7% of all bytes, 286 groups of byte-identical payloads account for 80.6% of the total, and the single largest contributor is five traces of exactly 820,000 bytes whose content is one sentence repeated to fill. It is the week this machine did nothing but develop and benchmark OMNI. A corpus of ordinary work reads far lower: the same harness on 6,656 traces in August 2026 read 2.7% and 14.9%.

Which commands benefit

classcallsinputfilters+ ledger
other3,70311.05 MB29.1%56.2%
file read (cat, sed, head, tail)88410.93 MB39.2%89.6%
search (grep, rg, find)600540 KB2.3%4.3%
git, gh696475 KB2.5%7.0%
infra (kubectl, az, docker)6570 KB0.0%6.8%
build and test3624 KB10.8%10.8%
aggregate5,98423.09 MB32.6%69.6%

infra reads 0.0% from the filters on purpose. It was 1.7% one release ago, bought by summarising kubectl get pods tables, which deleted the pod names that were the answer. That saving is gone and the rows are back (#562). What remains for infra is the ledger, which folds a listing the agent has already seen and needs the rows intact to do it.

By shell shape:

formcallsinputsaved
bare program78210,683,92440.2%
chain2,0249,843,90132.5%
cd prefix1,6551,476,7270.4%
VAR= assignment952567,1350.4%
pipe only571514,9624.6%

Top commands by input bytes, filters only:

commandcallsinputoutputsaved
tail4419,558,2725,599,66641.4%
zsh2828,391,1025,202,44338.0%
cd1,7271,503,8731,497,2940.4%
cat119770,972442,28542.6%
export535429,265428,7310.1%
grep447410,575402,8041.9%
sed217381,331381,3310.0%
git401262,113256,7862.0%
gh238145,950140,0954.0%
kubectl6871,12971,1290.0%

Byte-sink and token-sink rankings disagree at the tail: bash enters the token top 15 where kubectl sits in the byte one.

Head to head, one corpus

Identical bytes into every arm. Versions: rtk 0.45.0, lean-ctx 3.9.18, caveman 1.1.0 (binaries bin-v1.0.0), headroom at cross_turn_dedup.py.

bytessavedclaimed
rtk pipe23,086,649 to 21,655,2776.2%
caveman tools compress23,086,649 to 21,516,7576.8%
omni, filters only23,086,649 to 15,557,82332.6%
lean-ctx compress23,086,649 to 11,678,97549.4%425 of 5,984
headroom dedup, our filters23,086,649 to 7,905,76465.8%
omni, with the ledger23,086,649 to 7,026,02169.6%
rtk + our ledger23,086,649 to 8,906,37661.4%
caveman + our ledger23,086,649 to 8,844,10561.7%

headroom is 3.8 points behind our ledger and that is the only close race here. Both arms run the same filters over the same blocks, so the gap is the dedup engine and nothing else.

lean-ctx beats our filters by 16.8 points, 49.4% against 32.6%, over 425 calls to our 236. That is not argued away: this corpus is a few enormous repetitive payloads, which is exactly the shape a deep-and-narrow compressor is built for.

No lean-ctx + our ledger row: its preview reports compressed_bytes and never emits the text, so that row could only be estimated.

Single fixtures

From tests/fixtures/, same build, reproducible by hand. “Delivered” includes the marker.

commandinputdeliveredsaved
docker build (heavy noise)9,207 B102 B98.9%
cargo build (large, successful)3,220 B62 B98.1%
cargo test (490 passed, 10 failed)16,515 B1,153 B93.0%
git status (dirty)496 B165 B66.7%
git diff (multi-file)397 B247 B37.8%
kubectl get pods (mixed)840 B840 B0.0%

kubectl get pods reading 0.0% is the design, not a gap. For one release it read 73.5%, because a summariser that had been shadowed since #110 became live when #510 retired the TOML layer, and a 10 row table arrived as three lines with seven pod names deleted. A count of pods cannot be turned back into a pod name (#562).

docker build is the opposite case and worth the contrast: 251 lines of per-layer DEBUG and INFO become docker build: ✓ complete (50 layers, 50 cached), and the build did succeed. Noise, not an enumeration.

Method

OMNI_BENCH_DB=~/.omni/omni.db \
  cargo test --release --test bench_replay -- --ignored --nocapture
corpusexecution_traces.raw_input, real usage, replayed. Not synthetic
populationcalls whose result reached a model. OMNI_BENCH_ALL=1 widens it
statesession: None, store: None, HOME at an empty temp dir
pathrun_inner, the same pipeline the hook and omni exec run, markers included
binaryrelease build
armsOMNI_BENCH_RTK, _LEANCTX, _CAVEMAN, _HEADROOM, each off unless it names a binary, so CI never needs a competitor installed

Terminal output is excluded, and it is worth two different headlines. On an installation carrying it, it was 68% of raw bytes: 79.1% including it against 43.3% model-facing. The harness and omni stats both counted it until that was fixed, and both now print which population they used.

Every figure comes from one run. This file once published 15.7% and 16.1% from two replays a day apart without saying so.

Every window closes. execution_traces prunes at 7 days, so this corpus is gone a week after it was measured. Hold one open with OMNI_TRACE_RETENTION_DAYS.

Old figures are deleted, not kept for comparison. Releases keep changing the rule that decides whether the ledger folds a run, so an older number describes a pipeline that no longer exists, and printing both invites a reader to read two programs as a trend.

Latency was not re-measured on this build, so no table is printed rather than an older one relabelled. The method that produced the last one: median of 12 runs per payload, release binary, end to end through the post-hook, against a fresh database and a large one. Payload size barely mattered; database size did. Measure by removal, never with a microbenchmark: a unit-test timer once said 66 ms for work an A/B on the release binary put at 34.3 ms.

What no figure here can tell you

Whether the removed lines were signal.

Measure your own

omni stats
omni stats --share

Both read the same aggregation, so the share card cannot drift from the report. Terminal output is excluded from both.

Where OMNI is going

Direction only. The queue lives on the Now / Next / Later board and the shipped history lives in CHANGELOG.md. Copying either one here is how an earlier version of this page spent six weeks announcing v0.6.0 as in progress while 0.6.8 shipped.

The goal

OMNI removes noise from what an agent reads, without removing the answer and without overstating what it removed.

Compression is the easy half. A distiller that deletes a whole kubectl table and reports 99% saved compressed perfectly and did the job wrongly. So the target is not a reduction percentage. It is output an agent can act on, next to a number a human can reproduce.

Three properties, in the order they win when they conflict:

  1. Never fabricate. A stage that recognised nothing hands back what it was given. A failed command passes through verbatim. Structured payloads are never touched.
  2. Never lose the answer quietly. Anything dropped leaves a marker and, where the content allows, a handle.
  3. Then compress, as hard as the first two allow and no harder.

The number that decides progress

Primary: context-window pressure for the same job. Conversation growth, turns before compaction, and the cost of recovering task state in a new chat. That is the meter a user watches and the one OMNI is bought to move.

Secondary: distill %. Always scoped by agent_id, always model-facing only. A diagnostic for one host’s pipeline, not a product claim.

Why the swap away from blended reduction. On the reporting corpus, 81% of calls are passthrough and correctly do nothing, so a blended percentage describes the command mix more than the product. terminal rows are TTY bytes no model reads. Prompt-cache reads bill about a tenth of fresh input, so bytes saved once are not dollars saved per turn. And on a flat-rate plan compression does not reduce a bill at all; what it buys is session lifetime and fewer re-runs.

The gate on any public headline number. It cites the agent_id it covers, the corpus it was measured on, and a command a reader can run to reproduce it. A figure that blends terminal with hook agents, or counts a rewrite the host never applied, does not ship.

Non-goals

Recorded with dates, because the useful part of a rejected option is the reason.

not buildingwhydecided
An HTTP proxy in front of the modelIt puts OMNI on the request path and routes the user’s API key through a local process. The hook is the product, and the absence of that friction is most of the advantage.2026-07-23
A model or ML compressor inside the pipelineHooks have a sub-10 ms budget. Nothing with an inference call meets it.2026-07-23
Chasing a higher reduction % with more aggressive distillersThe failure mode this project keeps shipping is a confident summary that deleted the answer. More aggression buys the number and costs the product, and on a host that cannot rewrite built-in tool output it buys nothing at all.2026-08-07
Claiming shell distillation on a Handoff-first or MCP-only hostThe host does not apply the rewrite, so the model reads the same bytes it always did. Saying otherwise is the same defect as a distiller reporting a saving it did not make.2026-08-07
Intercepting a host’s shell by denying it and returning output as a hook messageTechnically possible on Cursor. It tells the agent its command was blocked, loses the exit code, moves execution semantics into OMNI, and bypasses the host’s approval flow.2026-08-07
Filter marketplace, team mode, remote archive, IDE extensionEcosystem features for a tool whose core claims are not all true yet. Worth reopening once the axes below are done.2026-07-29
A user or project filter tier on diskIt let a checkout decide what an agent is shown, behind a trust gate that hashed one file and admitted another. Deleted rather than repaired, and the whole layer was worth 804 bytes over 6,656 commands.2026-08-11

The three axes

A change that moves none of these can still be worth making, but it is maintenance, not direction.

1. Correctness: nothing asserted that was not parsed

Closed. The invariant moved off the authors and into the trait: distill returns Option<String>, so a distiller that parsed nothing returns None and the caller hands back raw bytes. It holds for all 12 by construction.

What is not closed is the class this axis exists for. Returning None proves a distiller knew it had failed. It proves nothing about one that parsed something and summarised it wrongly.

Check: no open bug describes OMNI asserting a result it did not parse, and that stays true across a full release cycle. This class has been filed against nine separate releases, so a quiet month is not evidence.

2. Coverage: the hook reaches the tools agents use

Closed for Claude Code. The post-tool matcher is Bash|Read|Grep|WebFetch, so the three distillers that had never run now do. Still open for hosts whose matcher vocabulary is narrower.

Check: the installed hook configuration names more than one matcher, and the database holds distillation rows for a tool other than Bash.

3. Proof: every published number can be reproduced

The open one. The blending is fixed: terminal runs are excluded from the model-facing figure and duplicate rows are gone. What remains is that numbers cannot outlive their corpus. execution_traces prunes at seven days, so any published figure stops being re-derivable a week after it is measured, which is the opposite of what this axis asks for.

Check: every published figure states its agent_id, its corpus, and the command that reproduces it.

Off the axes

Dependency and CI hygiene, README translation sync, dead-code removal, packaging and release mechanics. Real work, regularly done, and deliberately not direction.

Contributing

The most useful contributions are a distiller for a tool not covered, a signal for a tool whose noise is line-shaped, and a reproduction of any case where OMNI’s output claims more than its input supports.

The third is worth more than it sounds. See CONTRIBUTING.md in the repository.

Releasing

make ci                          # fmt + clippy + test + security + binary-check
make bump VERSION=x.y.z
make release VERSION=x.y.z
make release-sha VERSION=x.y.z   # after the tag has actually built

The order, and why it is not negotiable

Cut the changelog first, then bump.

bump_version.sh does not touch CHANGELOG.md, and build.rs counts what is still uncut in the tree it compiles: the bullets under ## [Unreleased] plus the fragments in changelog.d/. Tag without folding them and the released binary tells every user [N UNRELEASED] … cut a tag. It accuses itself.

So:

make changelog-cut VERSION=x.y.z   # folds changelog.d/ into ## [x.y.z] - <date>
git commit -am "docs(changelog): cut x.y.z"
make bump VERSION=x.y.z

A correctly cut build prints omni vx.y.z [AHEAD/RC] with no UNRELEASED line. Verify that before pushing the tag.

The half of that line to trust afterwards is the missing UNRELEASED, not the label. guard::update::get_status caches the newest known release in ~/.omni/update_cache.json for 14400 seconds, so for four hours after a tag a machine that ran omni doctor beforehand still holds the previous version and reports Ahead whatever it is running. Observed on 0.7.5, where the freshly installed release printed [AHEAD/RC]. build.rs computes the unreleased count from the tree with no cache, so that half is always current; delete the cache file if you want the label to mean something.

Day to day, the entry goes in changelog.d/<issue>.<section>.md as the work merges, not into CHANGELOG.md and not at tag time. One file per entry means two branches never write the same path, which is what stopped every parallel branch conflicting on ## [Unreleased]. The format is Keep a Changelog and SemVer, and the entries here are unusually detailed on purpose: each states the measured evidence, the wrong number that was published, and the mechanism. A one-line entry is a regression in that file’s quality.

The first cut needed one manual tidy and it is done. 0.7.5 folded three fragments beside seven bullets written into ## [Unreleased] before the convention existed, and arrived with two ### Changed and two ### Fixed under one version heading. Merging those four into two was the only hand edit. Check a cut by word count rather than by eye: 2,252 words across the old section plus the fragments, 2,252 in the folded section. A reordering that drops a bullet body looks correct in a heading-level diff.

CI green does not mean the release will build

The 0.6.2 tag produced no binaries at all. release.yml asked for stable per cross target while rust-toolchain.toml pinned a version, so every cross-compile died with can't find crate for core before compiling a line. ci.yml stayed green throughout, because it only builds host-native.

The fix is that cross targets belong in rust-toolchain.toml, and its targets list has to stay in sync with the release matrix.

After tagging, watch the release workflow actually produce artifacts before running make release-sha or announcing anything.

Things that look like failures and are not

omni-release.sh ends in an interactive read -p, so an automated run has to pipe echo y | into it.

It pushes main and the tag together. main is branch-protected, and a maintainer token bypasses it: the push prints “Changes must be made through a pull request” and succeeds anyway with rc=0. That line is not an error.

The Homebrew step

update_homebrew_sha.sh pushes to two repositories: the tap, and omni.rb back to main. Check the tap clone is clean and synced with its remote first, or the run aborts partway and leaves the formula half updated.

Afterwards, verify the formula’s SHAs against the release’s published SHA256SUMS rather than trusting the script’s own success line, then confirm:

brew info fajarhide/tap/omni      # expect: x.y.z → stable <new>

Before merging anything into a release

CI green is not review-clean. Read the review comments, automated and human, validate each one against the code rather than assuming the reviewer is right or wrong, and fix or reply. A green pipeline says the tests passed. It says nothing about a correctness bug a reviewer flagged.

Branch shape

One branch per batch, not per issue. N parallel branches cost N full CI runs of about eleven minutes each, serialised. Batch a lane into one branch, one commit per issue, one pull request with several Closes #N lines.

Split only when a reviewer would genuinely need them apart, or when one is risky enough to be reverted alone.

That conflict used to be CHANGELOG.md, every time. It is gone: entries are files in changelog.d/ now, and two branches never write the same path. What remains is the CI cost, which is why batching still pays.

Closes #N must be in the pull request body before the merge. GitHub evaluates the keyword at merge time only; adding it afterwards does nothing, silently.

Hermes Agent

OMNI plugs into Hermes twice: a plugin on the hook path, and the MCP server.

layermechanismwhat changes
hooks~/.hermes/plugins/omni-signal-engine/__init__.py calling omni --pre-hook, --post-hook, --session-startterminal tool output is distilled before it enters Hermes’ context
MCPmcp_servers.omni running omni --mcpOMNI’s MCP tools become first-class Hermes tools

Prerequisites

brew install fajarhide/tap/omni
omni --version
omni doctor

export HERMES_VENV="${HERMES_HOME:-$HOME/.hermes}/hermes-agent/venv"
export HERMES_PY="$HERMES_VENV/bin/python"
"$HERMES_PY" --version      # 3.11 or newer

The venv Python is needed because hermes plugins enable runs inside it.

Install

omni init --hermes
hermes plugins enable omni-signal-engine
hermes gateway restart
"$HERMES_PY" -m pip install hermes-omni-plugin

omni init --hermes is idempotent. It installs the plugin scaffold, registers the MCP server in ~/.hermes/config.yaml if it is not already there, enables Hermes compression when that is safe, and writes Hermes-oriented defaults to ~/.omni/config.toml without overwriting an existing OMNI config.

Use either hermes-omni-plugin or the omni init --hermes scaffold, not both at once, or you get duplicate plugin registrations.

Config

# ~/.hermes/config.yaml

plugins:
  enabled:
    - omni-signal-engine

mcp_servers:
  omni:
    command: "/opt/homebrew/bin/omni"
    args: ["--mcp"]
    env:
      OMNI_AGENT_ID: "hermes"

compression:
  enabled: true
  threshold: 0.50     # compress at 50% context usage
  target_ratio: 0.20  # keep 20%

Three things have to be true: plugins.enabled contains omni-signal-engine, mcp_servers.omni points at the real binary, and compression.enabled is on so Hermes’ own compaction and OMNI’s pressure warnings line up rather than fighting.

OMNI_AGENT_ID: "hermes" matters more than it looks. Without it, Hermes’ rows blend with every other host’s and no figure about either is meaningful.

Verify

omni doctor

hermes plugins list | grep omni        # expect: omni-signal-engine enabled
hermes tools list | grep mcp_omni_     # expect 25 tools, after a restart

Then a functional check on a real fixture:

cat tests/fixtures/cargo_test_500.txt | omni --post-hook 2>&1 | head -20
# passing test lines stripped, failures preserved

For a live test, run something noisy through Hermes’ terminal tool (terminal("npm install", timeout=120)) and compare the tool result size against raw npm output. Confirm with omni stats.

Count the tools rather than trusting a number written down. Earlier versions of this guide said 27, which came from grepping the server source; one of those strings is a filter name, not a tool. The hermes tools list above is the count.

Where OMNI helps and where it does not

outputOMNI’s effect
npm install, cargo build, docker buildlarge, 70% and up. Progress, cache hits and layer hashes are pure ceremony.
test runslarge. The verdict and the failures survive, the ok lines do not.
file readsnothing from the filters, a great deal from the ledger on re-reads
kubectl -o json, terraform plansnothing, deliberately. Structured payloads pass through.
short commandsnothing, or slightly negative. The marker costs more than the saving.

Use the MCP tools as Hermes’ controls over all of it: omni_explain_savings to see what a recent command actually cost, omni_retrieve to get folded content back, and omni_budget to see where the session’s tokens went. That tool is outside the set advertised by default, so it needs OMNI_MCP_TOOLS=all.

After a Hermes upgrade

hermes plugins list | grep omni
hermes tools list | grep mcp_omni_
omni doctor

An upgrade can reset plugins.enabled or move the venv. Both fail quietly: the plugin simply stops being called, and nothing announces it.

Loop engineering

Running an agent in a loop, where each iteration adds to a context window that does not grow. OMNI’s part is tracking what the loop has spent and carrying memory across iterations that would otherwise reset.

Setting a loop up

export OMNI_LOOP_ID=$(uuidgen)
export OMNI_LOOP_GOAL="Migrate the billing service off the legacy queue"
export OMNI_LOOP_BUDGET=100000
export OMNI_LOOP_ITERATION=0
variableconstraint
OMNI_LOOP_IDalphanumeric and dash, 64 characters
OMNI_LOOP_GOAL500 characters, no shell metacharacters
OMNI_LOOP_BUDGETtoken budget per iteration, up to 10M
OMNI_LOOP_ITERATIONcurrent iteration, default 0
OMNI_SUBAGENT=1sub-agent mode
OMNI_AGENT_IDidentity, so traces stay separable

Budget

The budget is estimated context window usage per iteration, not a spend limit.

loop shapebudgetwhat OMNI does
quick fix, 1 to 5 iterations200,000passive tracking
feature work, 5 to 20100,000active distillation, engrams
large refactor, 20 to 10080,000aggressive distillation, predictive warnings
marathon, 100+60,000maximum compression, loop memory persistence

Warnings fire at 65% and critical at 82%, adjustable with OMNI_PRESSURE_WARN and OMNI_PRESSURE_CRITICAL.

Do not set a budget above 1M: warnings will never fire before real exhaustion. Do not set one below 30K: the agent will compact constantly and lose short-term memory.

The goal string also shifts distillation aggressiveness. A goal containing “test” preserves test detail, “debug” keeps error context, “refactor” compresses harder.

Tools an orchestrator calls

None of these are advertised by default. OMNI tells a host about the tools its tier actually uses, and the loop tools are outside that set, so an orchestrator that calls them needs OMNI_MCP_TOOLS=all in its environment. omni doctor prints which set is in force. The MCP tools reference has the per-tier lists.

toolwhen
omni_loop_statusonce before each iteration, the cheapest full picture
omni_budget_statusbefore anything expensive
omni_set_loop_contextwhen the goal or scope shifts mid-loop
omni_loop_memoryread and write memory that survives a session restart
omni_verifyas a checker, to evaluate the maker’s recent work

Maker and checker

Two agents, one shared context layer.

LOOP_ID=$(uuidgen)

# the loop tools are outside the default advertised set
export OMNI_MCP_TOOLS=all

# maker
export OMNI_AGENT_ID=maker OMNI_LOOP_ID=$LOOP_ID
claude "Implement: $GOAL"

# checker
export OMNI_AGENT_ID=checker OMNI_SUBAGENT=1
RESULT=$(claude "Verify the implementation of: $GOAL. Use the omni_verify tool.")

case "$RESULT" in
  *PASS*) echo "verification passed" ;;
  *)      echo "checker found issues" ;;
esac

Distinct OMNI_AGENT_ID values are what keep the two from contaminating each other. Traces are tagged by agent, so omni_verify can read across sessions while writes stay isolated.

Four things that make it work: give the checker specific measurable criteria, keep last_n_calls between 5 and 20, escalate to a human after three consecutive checker failures, and remember that every interaction is logged so the audit trail is real.

Monitoring

omni stats                 # real-time
omni stats --detail
omni stats --json          # for an orchestrator to read
omni doctor                # health

omni handoff is not a CLI subcommand. It was removed. The omni_handoff MCP tool is unchanged, so session export is reachable from an MCP client rather than a shell.

A caution about the numbers

Every figure a loop reports is scoped by agent_id. If the orchestrator and the agents share one id, the maker’s savings and the checker’s are one number and neither is meaningful. Set the id per role before the first iteration, not after you notice.