Skip to content

fix(hooks): a folded reply carries no second accounting of itself - #777

Merged
fajarhide merged 3 commits into
mainfrom
fix/775-banner-pays-for-its-own-cut
Sep 4, 2026
Merged

fix(hooks): a folded reply carries no second accounting of itself#777
fajarhide merged 3 commits into
mainfrom
fix/775-banner-pays-for-its-own-cut

Conversation

@fajarhide

@fajarhide fajarhide commented Sep 4, 2026

Copy link
Copy Markdown
Owner

A folded reply carried a second accounting of itself, and the two did not add up to the
input. On a re-run of git log --oneline -40, through the installed 0.7.9 with an isolated
database:

[OMNI: 22 lines identical to an earlier run, omni retrieve 87538eab3c7f9929]
[OMNI: 17 lines identical to an earlier run, omni retrieve c7ca76c26123fa4d]
<one surviving line>
[OMNI: 1 bytes omitted, omni retrieve 91e7492e2c005907 for full output]

22 + 17 + 1 = 40, the whole input, and then a fourth line claims a byte more. That is the
false-claim class: nothing was missing and the output says otherwise.

The mechanism is narrower than the issue's own diagnosis. The banner's numbers come
from the distiller, correctly and by design (#519). What did not was the test deciding
whether to print it, which read final_out, and by then final_out is the ledger's
output. So the ledger's saving bought a marker describing the distiller's cut, and here
that cut was one byte, almost certainly a trailing newline.

Three commits, because the first two were wrong in ways worth keeping in the history.

  1. Weigh the marker against distilled_len. Correct, and on its own it turned a 3,257 byte
    reply folded to 325 into a passthrough: the else arm throws the whole reply away, which
    was right when the distiller was the only stage to touch it and is not right now the
    ledger runs first.
  2. Keep the fold when the marker cannot pay for itself, and pass through only when the
    ledger left the reply alone. Then never_hands_back_more_bytes_than_the_command_produced
    went red at 104 bytes back for 99 raw. That test drives process_payload with no store,
    so the ledger never runs and the new arm was not involved at all: the leak was
    [Partial signal], appended after the distiller's numbers are taken, whose cost had
    silently left the affordability test.
  3. Require both tests. The marker must be paid for by the cut it describes and the reply
    must still be smaller than the command's own bytes. Those are different questions and
    answering only one of them is what each earlier commit did.

After all three the same command delivers 252 bytes for 3,257, and the markers sum to
exactly 40 of 40.

Also 1 bytes and 1 lines. Singulars.

The issue's second claim is not fixed and the issue stays open for it.
[Partial signal] over a complete answer did not reproduce in three attempts on 0.7.9 with
isolated databases: the post-hook on the reported command, the same through omni exec, and
a constructed 130 line log payload aimed at the partial route. It needs route == Soft
together with a partial rather than whole-output fold, and every fixture I built folded
whole or never reached Soft. Not a verdict that it is unreal; the reporter saw it. The
detail is in the issue.

Verification: make ci green, smoke_test.sh 70/70, and the reported command re-run
against the release binary.

Refs #775

Greptile Summary

This PR adjusts post-tool rewind affordability so ledger savings cannot fund a marker describing a separate distiller cut, preserves useful ledger folds when that marker is rejected, and fixes singular marker units.

  • Measures marker affordability against both the distiller output and the final delivered output.
  • Tracks whether the ledger folded the reply and retains that fold when it remains smaller than the raw command output.
  • Adds regression coverage for repeated git log output and documents the fix in the changelog.

Confidence Score: 4/5

The PR should not merge until retained ledger folds preserve a retrieval path for any bytes removed earlier by the distiller.

The new fallback keeps a reduced host-visible result while clearing the only handle that archives the raw pre-distillation output, making the distiller's omitted bytes unreachable from the response.

Files Needing Attention: src/hooks/post_tool.rs

Important Files Changed

Filename Overview
src/hooks/post_tool.rs Corrects cross-stage marker accounting and adds regression coverage, but the new fold-preservation branch can strand bytes removed by the distiller without a visible retrieval handle.
changelog.d/775.fixed.md Documents the false double-accounting fix, fold preservation, and singular marker wording.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Raw[Raw command output] --> Distiller[Distiller]
  Distiller --> Distilled[Distilled output]
  Distilled --> Ledger[Session ledger fold]
  Ledger --> Final[Final folded output]
  Raw --> Archive[Raw-output rewind archive]
  Archive --> Marker{Marker affordable?}
  Distilled --> Marker
  Final --> Marker
  Marker -->|Yes| DeliverMarker[Deliver fold plus rewind marker]
  Marker -->|No, ledger folded| DeliverFold[Deliver ledger fold]
  Marker -->|No fold| Passthrough[Keep raw host output]
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Codex Fix All in Cursor Fix All in Conductor

Prompt To Fix All With AI
### Issue 1
src/hooks/post_tool.rs:1047-1063
**Distiller cuts lose recovery**

When the distiller removes less than its marker costs and the ledger also folds the reply, this branch keeps the folded output but clears the only handle covering the raw pre-distillation content. The ledger handles recover only runs from the already-distilled text, so the agent cannot recover bytes removed by the distiller.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(hooks): the marker has to pass both ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

…the fold

Two accountings of one payload landed in one blob and their sum exceeded the
input. On a re-run of git log --oneline -40 the distiller cut a single byte,
the ledger folded the reply to a few lines, and the banner announced
'1 bytes omitted' on top of fold markers that already covered all 40. Every
number was true and the arithmetic a reader can do was not.

The affordability test read final_out, which is the ledger's output, so the
saving the ledger made bought a marker describing the distiller's cut. It
reads distilled_len now: the numbers in that marker describe the distiller,
so the test that decides whether to print it has to describe the distiller.

The first version of this fix stopped there and broke the saving. The else arm
throws the whole reply away, which was correct when the distiller was the only
stage that touched it and is not correct now the ledger runs first: the same
re-run turned a 3,257 byte reply folded to 325 into a passthrough. It passes
through only when the ledger left the reply alone.

Also: 1 bytes, 1 lines. Singulars.

Closes #775
… other

The first cut of this asked only whether the distiller's cut could pay for the
marker, and dropped the question of whether the reply that results is still
smaller than the command's own bytes. Those are different questions and the
second one covers bytes this function appends after the distiller's numbers
are taken, including a [Partial signal] banner.

Caught by never_hands_back_more_bytes_than_the_command_produced, which drives
process_payload with no store, so the ledger never runs and my new arm was not
even involved: 104 bytes back for 99. The repo's own invariant found it, not
review, and not me.
Comment thread src/hooks/post_tool.rs
Comment on lines +1047 to +1063
} else if ledger_folded && final_out.len() < content.len() {
// The marker cannot pay for the distiller's cut, so it is not
// printed. The ledger's fold is a different saving with its own
// markers and its own handles, and throwing it away here cost the
// whole of it: on a re-run of `git log --oneline -40` this arm
// turned a 3,257 byte reply that had been folded to 325 into a
// passthrough, because the distiller had cut one byte.
//
// Nothing is left unreachable. The handle is cleared because no
// marker names it, and every folded run carries its own.
//
// The size test is not decoration. `never_hands_back_more_bytes_than_
// the_command_produced` caught the first version of this arm handing
// 104 bytes back for 99: a fold on a tiny payload can cost more than
// it saves, and the old passthrough was what capped that. Keeping the
// fold is only right when the fold is smaller.
rewind_hash.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Distiller cuts lose recovery

When the distiller removes less than its marker costs and the ledger also folds the reply, this branch keeps the folded output but clears the only handle covering the raw pre-distillation content. The ledger handles recover only runs from the already-distilled text, so the agent cannot recover bytes removed by the distiller.

Knowledge Base Used: Tool hook integration

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/post_tool.rs
Line: 1047-1063

Comment:
**Distiller cuts lose recovery**

When the distiller removes less than its marker costs and the ledger also folds the reply, this branch keeps the folded output but clears the only handle covering the raw pre-distillation content. The ledger handles recover only runs from the already-distilled text, so the agent cannot recover bytes removed by the distiller.

**Knowledge Base Used:** [Tool hook integration](https://app.greptile.com/weekndlabs/-/custom-context/knowledge-base/fajarhide/omni/-/docs/tool-hook-integration.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Cursor Fix in Conductor

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, and it is the rule this pipeline is not allowed to break, so it is worth being exact
about the size and about why I am not fixing it in this PR.

The bound. That arm is only reached when distilled_len + marker.len() >= content.len(),
so the distiller's cut is smaller than the marker that would announce it: under about 80
bytes. In the reported repro it was a single byte, almost certainly a trailing newline.
Small, and still unannounced, which is the part that matters.

Three fixes tried, and each one broke something the repository already guards.

  1. Keep the handle and print the marker anyway. That is the double accounting this PR
    exists to remove.
  2. Refuse a cut too small to announce, before the ledger runs, so there is nothing
    unreachable to keep. a_dropped_reply_books_no_saving went red: reverting final_out
    before record_trace makes the raw and distilled columns identical, which silently
    removes the only corpus that measures distiller behaviour without going through OMNI's
    own books. That test's own comment says a previous fix made exactly this mistake.
  3. Snapshot the distiller's output for the trace and revert the delivered text. That works
    only if the trace is meant to hold the pre-ledger string, and today it holds the
    post-ledger one, so the change would quietly alter what execution_traces means.

So the question is a design call rather than a patch: either the trace's distilled
column becomes explicitly pre-ledger, or the ledger is given the raw content when the
distiller's cut is below the floor, or the sub-marker cut is accepted as a documented and
bounded loss. The first two are real changes with their own measurements; the third needs
saying out loud rather than leaving in a comment.

What this PR does ship is strictly better than main: the false claim is gone, the
arithmetic sums, and the fold survives. What it does not fix is a pre-existing class made
visible by fixing that, and it is now written down instead of discovered again in six
months.

Filing it as its own issue with these three options and the bound, and leaving this PR to
the defect it names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That distinction is correct. The fallback is bounded to cuts smaller than the marker cost, but those bytes are still genuinely unreachable once the raw rewind handle is cleared. Printing the marker would reintroduce the double-accounting defect, while moving the distiller boundary would change the established execution_traces.distilled semantics or break a_dropped_reply_books_no_saving.

I agree this is a valid follow-up design issue rather than a reason to block this PR: this change fixes the false accounting while preserving the ledger fold, and the remaining loss is explicit, bounded, and documented. The follow-up should capture the invariant that every distiller-removed byte must either have a retrieval handle or be intentionally accounted for, along with the three design options you listed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant