The payload that never landed: OMNI v0.6.5
v0.6.5 has one entry, and it is the same bug as the one v0.6.4 already claimed to have fixed.
The half-fix
v0.6.4 fixed #158:
the Claude Code hook was writing its replacement under
hookSpecificOutput.updatedResponse, and the key the host reads is
updatedToolOutput. Correcting the key was necessary and it was not sufficient,
because updatedToolOutput is not a free-form object.
Claude Code runs tool.outputSchema.safeParse(value) keyed on the tool that
ran. There is one shape per host tool. OMNI was sending the MCP shape,
{status, result}, to all of them. Bash’s schema wants
{stdout: string, stderr: string, interrupted: boolean, …}, so every payload was
rejected, the host restored the original bytes, and the UI rendered
PostToolUse:Bash hook warning. Meanwhile the sibling additionalContext went
through and printed a saving for a distillation that had just been discarded.
The observable behaviour never changed across the fix: savings reported, nothing applied. It survived two releases because the only thing that changed was the field name in an error nobody was reading.
Measured from the host, not from our own numbers
Counting rejections in the Claude Code session transcript rather than trusting
OMNI’s telemetry: 27 of 34 Bash calls in a single session were rejected, each
with stdout: expected string, received undefined, stderr: …,
interrupted: expected boolean, received undefined. One of them printed
-3247tok this call | 67% compression for 300 npm WARN lines the agent
received in full.
The 7 quiet calls are the ones where OMNI correctly emits nothing at all: a non-zero exit, and the format-safe gate.
Reply in the shape you were spoken to in
The fix is not a table of per-tool schemas. The reply is now the response
object that arrived, with stdout swapped and every other key preserved.
interrupted, isImage, backgroundTaskId, persistedOutputPath and
timedOutAfterMs are all schema members, and rebuilding a minimal object would
have failed validation exactly as the old shape did. Reading the shape off the
incoming payload keeps it correct for tools this code has never seen, and there
is no table to maintain.
stderr is blanked rather than echoed, because normalisation folds a
non-empty stderr into the text that gets distilled, so returning the original as
well would show it to the agent twice. Blanked rather than dropped, because the
schema requires the member to be a string.
Payloads that arrive without a host response object, from Codex, Pi or a generic MCP client, keep the MCP shape deliberately. Those hosts’ contracts were not investigated, and guessing at a second one is how the first was got wrong.
Verified end to end through the built binary rather than the struct: the same
300-line payload now returns {stdout, stderr, interrupted, isImage, backgroundTaskId} with no status or result, 18,490 bytes down to 1,819, and
the reported 90% matches a measured 90.2%. A stashed baseline confirms the
distilled text is byte identical before and after. This change moves the
envelope only.
The test that let both halves through
It asserted "status":"success", which is the same field name it had just
serialised. That is why a wrong key and then a wrong shape both passed it. The
replacement asserts against the host’s schema, and specifically that status and
result are absent.
One consequence, filed rather than hidden
Making the payload actually apply means it delivers, for the first time, an unmarked 30-line truncation in the JS/TS distiller that no agent had ever seen. That is fixed in v0.6.6, along with six other ways OMNI could invent a result or delete the answer.