The wrong path scored better: v0.7.3

Three fixes about mistaken identity. A tool name, a session and a command’s output were each taken for something they were not, and in every case the substitution was invisible from outside: the wrong path scored a higher saving, two sessions merged with no error, and a probe whose answer had been deleted read exactly like a probe that found nothing.

A tool called bash never reached the distiller for Bash

distil_tool_reply matches Bash, Read, Grep and WebFetch exactly. The Pi and OpenAI-shaped branches normalise the tool name before that check. The ClaudeCode branch, which is the shape a third-party integrator is most likely to send, passed the name straight through, so a payload arriving as bash fell to the generic arm and reached neither the per-tool distiller nor the ledger.

What kept it alive is that the generic arm still shortens. On a real shell corpus, bash reported 59.9% and Bash reported 37.6%, so the broken path looked like the better one on every dashboard that counts bytes.

The gap is the whole point. The generic arm keeps the first thirty lines and discards the rest, which is a large number and an arbitrary cut. The per-tool path folds only what the agent has already been shown, which is a smaller number and a claim that is true. A reduction percentage cannot tell those two apart, and this is what that costs.

Two sessions in the same millisecond were one session

The session id was timestamp_millis() and nothing else, and the insert is INSERT OR REPLACE INTO sessions, so two sessions minted in the same millisecond kept one of the pair and lost the other without an error. On a machine running several agents, two SessionStart hooks landing together is not an edge case, it is Tuesday.

The id is now {millis}-{pid}-{counter}, all standard library. The milliseconds stay at the front because four call sites slice session_id[..8] to display it.

This does not make the id meaningful, and it was not meant to. It is still minted state rather than an identity the host handed us, which is exactly why the ledger scopes on host_session() instead of on this.

kubectl run output was read as kubectl output

wraps_another_command knew about exec and not about run, so kubectl run --rm -i -- <cmd> was routed to the kubectl grammar and the container’s own stdout was parsed as though kubectl had printed it. An eight-line probe came back as pod "omni-repro" deleted and a marker: the one line kubectl writes itself survived, and the eight lines the command existed to produce did not.

The direction of that failure is the bad one. A probe that returns nothing is a plausible result, and nothing in the delivered text separates it from an answer that was thrown away.

The two doors disagreed, which is why an early probe found nothing. The same payload through hooks::post_tool was never rewritten, so only hooks::pipe destroyed it, and a post-hook reproduction reported no defect at all. run now counts as a wrapper for kubectl, docker and podman, the same as exec.

The corpus cannot arbitrate that widening and it is worth saying so: 6,656 recorded commands hold two podman run and no kubectl run. The change to the other two runtimes is reasoning from the shape of the command, not from measured traffic.

Full notes are in the 0.7.3 changelog.

brew upgrade omni     # or: omni update