Skip to content

Reduce compaction context while preserving task state - #873

Merged
anandh8x merged 7 commits into
mainfrom
feat/conversation-state-compaction
Aug 8, 2026
Merged

Reduce compaction context while preserving task state#873
anandh8x merged 7 commits into
mainfrom
feat/conversation-state-compaction

Conversation

@anandh8x

@anandh8x anandh8x commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • project older conversation history into a bounded semantic transcript before the summarizer call
  • omit reconstructible successful tool output while retaining user intent, assistant decisions, tool actions, concise failures, and the recent verbatim tail
  • preserve bounded constraints, unresolved failures, permission decisions, artifact references, plans, edits, loaded tools, skills, and project instructions across repeated compaction
  • carry the previous prose summary separately so older decisions remain available
  • apply the same behavior to automatic context-pressure compaction and manual /compact

Why

Long coding sessions can accumulate large file reads and command output. The previous compaction path sent that entire reconstructible payload to the summarizer, increasing token cost and latency even though the final replacement was already small.

Controlled benchmark

The refreshed benchmark uses a deterministic 616,309-byte corpus assembled from eight real Zero source files and the same recorded transcript for both measurements. It measures the raw summarizer input against the current semantic projection over five sequential runs. The temporary harness was removed after the run and no report is committed.

Measurement Raw input Projected input
Corpus bytes 616,309 616,309
Summarizer input characters 617,175 1,006
Estimated input tokens 128,423 223
Median local preparation 0.000041 ms 0.047 ms
Allocated memory 0 B ~16 KB

This reduces estimated summarizer input by 99.83% while adding about 0.047 ms of local projection work.

The replay assertions are structural checks on representative sessions: they verify that user intent, decisions, exact failures, active plans, ask-user exchanges, changed files, and the recent tail survive. They do not claim to measure model-summary quality across every provider or workload.

Validation

  • make fmt-check
  • go vet ./...
  • focused compaction tests in internal/agent, internal/sessions, and internal/tui
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • make vulncheck
  • git diff HEAD --check
  • manual TUI verification of /compact, continued conversation, /resume, and exact constraint recall

go test ./... still reports three unrelated failures that reproduce outside the changed paths: two CLI doctor tests return exit code 3, and the suggestion-overlay test matches the word compaction in the checkout path or branch name. make lint-static reports two stale .pr867-validation paths that are not present in this branch.

Summary by CodeRabbit

  • Improvements
    • Improved conversation compaction to preserve user intent, summaries, responses, tool activity, errors, constraints, approvals, unresolved issues, changed files, and artifact details.
    • Reduced unnecessary tool output during summarization while retaining important context and lowering token usage.
    • Improved continuity across repeated compaction cycles by tracking runtime evidence and resolved failures.
    • Added prompt-free compaction planning, accurate prompt metrics, truncation reporting, and clearer fallback handling.
  • Bug Fixes
    • Prevented important task details from being lost during summarization.
    • Improved handling and visibility of failed or aborted tool actions.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Compaction now projects bounded conversation content before summarization. Task state records constraints, tool failures, approvals, changed files, and artifacts. Session replay and the TUI use normalized messages and shared summarization metadata.

Changes

Compaction and runtime evidence

Layer / File(s) Summary
Runtime evidence contracts
internal/zeroruntime/types.go, internal/agent/task_state.go, internal/agent/loop.go, internal/agent/*_test.go
Runtime messages carry error status and changed files. Task state records bounded constraints, failures, approvals, and artifacts from tool and permission events.
Projected compaction input
internal/agent/compaction_projection.go, internal/agent/compaction.go, internal/agent/*compaction*_test.go
Compaction retains bounded user intent, summaries, relevant tool information, errors, questions, and changed files. It omits reconstructible tool output and reports truncation metadata.
Preserved task reconstruction
internal/agent/compaction_preserve.go, internal/agent/compaction_preserve_test.go
Compaction merges and bounds preserved evidence across cycles. It removes resolved failures and deduplicates comparable entries.
Shared summarization API
internal/agent/compaction.go, internal/agent/compaction_test.go, internal/agent/compaction_metadata_test.go
The shared summarization path validates callbacks, projects messages, falls back when needed, trims summaries, and returns projected character and truncation metadata.
Session compaction integration
internal/sessions/replay.go, internal/sessions/replay_test.go, internal/tui/session_controls.go, internal/tui/session_controls_test.go
Session events become normalized runtime messages. Prompt-free planning and validated event resolution feed the shared summarization flow. TUI compaction records prompt size and truncation state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentLoop
  participant TaskState
  participant SessionReplay
  participant TUI
  participant SummarizeCompactionMessages
  AgentLoop->>TaskState: record tool and permission evidence
  TUI->>SessionReplay: plan compaction with SkipPrompt
  SessionReplay-->>TUI: normalized compaction messages
  TUI->>SummarizeCompactionMessages: projected messages
  SummarizeCompactionMessages-->>TUI: summary and truncation metadata
Loading

Possibly related PRs

  • Gitlawb/zero#166: Extends the earlier compaction APIs, preservation logic, session replay, and TUI compaction flow.
  • Gitlawb/zero#484: Overlaps in compaction and preserved task-context behavior.
  • Gitlawb/zero#853: Overlaps in task-state and permission-event handling.

Suggested reviewers: gnanam1990, vasanthdev2004, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: reducing compaction context while preserving task state.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/conversation-state-compaction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
internal/agent/compaction_projection.go (2)

77-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the nested emptiness check.

The inner if is the only effect of the outer if. One condition states the same rule.

♻️ Proposed simplification
-	if len(sections) == 0 {
-		if previousSummary == "" {
-			return nil
-		}
-	}
+	if len(sections) == 0 && previousSummary == "" {
+		return nil
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/compaction_projection.go` around lines 77 - 81, In the
surrounding section-handling logic, collapse the nested checks into a single
condition that returns nil when sections is empty and previousSummary is also
empty. Preserve the existing behavior for nonempty sections or a nonempty
previousSummary.

159-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse one UTF-8-safe truncation helper.

clipBytes repeats the logic of capTaskEvidence in internal/agent/task_state.go (lines 289-299). Both trim, reserve three bytes, walk back to a rune boundary, and append "...". Keep one helper with a byte limit parameter.

clipBytes also computes end := limit - 3 without a floor. If a future caller passes a limit below 3, text[:end] panics. A shared helper with a guarded floor removes that risk.

♻️ Proposed shared helper
 func clipBytes(text string, limit int) string {
 	text = strings.TrimSpace(text)
 	if len(text) <= limit {
 		return text
 	}
-	end := limit - 3
-	for end > 0 && end < len(text) && text[end]&0xc0 == 0x80 {
-		end--
-	}
-	return strings.TrimSpace(text[:end]) + "..."
+	end := max(limit-3, 0)
+	for end > 0 && !utf8.RuneStart(text[end]) {
+		end--
+	}
+	return strings.TrimSpace(text[:end]) + "..."
 }

Then call clipBytes(value, maxTaskEvidenceBytes) from capTaskEvidence and delete the duplicated loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/compaction_projection.go` around lines 159 - 169, Consolidate
the duplicated UTF-8-safe truncation logic from clipBytes and capTaskEvidence
into one shared byte-limit helper, retaining trimming, ellipsis handling, and
rune-boundary safety. Guard the reserved ellipsis calculation so limits below
three cannot produce a negative slice index. Update capTaskEvidence to reuse
clipBytes with maxTaskEvidenceBytes and remove its duplicated truncation loop.
internal/agent/task_state.go (1)

325-360: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid in-place values[:0] filtering on stored state.

appendBoundedUnique and removeTaskFailure rewrite the caller's backing array. Today snapshot() copies every evidence slice, so no external holder observes the mutation. The pattern still couples helper behavior to that copy, and mergeTaskFailures in internal/agent/compaction_preserve.go repeats it on parsed prior state. Allocate a new slice in these helpers instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/task_state.go` around lines 325 - 360, Update
appendBoundedUnique and removeTaskFailure to allocate fresh result slices
instead of filtering through values[:0], preserving the existing ordering and
bounded behavior. Also replace the same in-place filtering pattern in
mergeTaskFailures with newly allocated slices so stored or parsed state is never
mutated through shared backing arrays.
internal/agent/task_state_test.go (1)

145-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where a different target succeeds.

The retry reuses the same arguments, so this test passes with or without argument identity in the failure key. Add a second failure from a non-command tool, for example read_file on a.go, then a success on b.go, and assert the failure for a.go remains. That case pins the fix for the key collision reported on internal/agent/task_state.go lines 234-247.

As per coding guidelines: "add a regression test for behavior changes".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/task_state_test.go` around lines 145 - 151, Extend the test
around taskStateEventToolResult to add a read_file failure for a.go, followed by
a successful read_file result for b.go with different arguments. Assert that the
unresolved failure for a.go remains while the b.go failure is resolved, covering
argument identity in the failure key and guarding the behavior in task state
failure tracking.

Source: Coding guidelines

internal/agent/compaction_preserve_test.go (1)

130-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use slices.Contains instead of a local helper.

go.mod declares Go 1.26.5, so slices is available. Remove containsString and call slices.Contains(state.Task.Constraints, want).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/compaction_preserve_test.go` around lines 130 - 137, Remove
the local containsString helper and replace its call sites with
slices.Contains(state.Task.Constraints, want). Add the slices import if needed,
preserving the existing containment behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/compaction_preserve.go`:
- Around line 577-589: Update mergeTaskFailures to deduplicate entries by
taskFailureState.Key, with newer entries replacing older ones while preserving
maxTaskEvidenceEntries; avoid mutating the input older slice by building
filtered state in newly allocated storage. Add a regression test in
compaction_preserve_test.go covering repeated compaction of the same failing
command with different summaries and asserting only the newest failure remains.

In `@internal/agent/loop.go`:
- Around line 190-196: Update the OnPermission wrapper in the task execution
flow to avoid calling task.observe from parallel read-batch worker goroutines.
Move permission-state observation to a synchronized non-worker path, or protect
the relevant task state with dedicated locking, while preserving invocation of
the original onPermission callback.

In `@internal/agent/task_state.go`:
- Around line 310-323: Update extractExplicitConstraints to collect every
matching line in order instead of returning after the first match, stopping once
maxTaskConstraints is reached. Preserve the existing line filters and return
behavior, and revise the relevant task-state test to assert that multiple
explicit constraints are retained.
- Around line 234-247: Update observeToolEvidence so failures from tools without
a command field remain distinguishable by including a stable, discriminating
argument or target in key; alternatively, only resolve existing failures when
command is non-empty. Preserve separate failure entries for different inputs to
the same tool, and pass the already-capped command directly into
taskFailureState.Command-related data instead of applying capTaskEvidence twice.

---

Nitpick comments:
In `@internal/agent/compaction_preserve_test.go`:
- Around line 130-137: Remove the local containsString helper and replace its
call sites with slices.Contains(state.Task.Constraints, want). Add the slices
import if needed, preserving the existing containment behavior.

In `@internal/agent/compaction_projection.go`:
- Around line 77-81: In the surrounding section-handling logic, collapse the
nested checks into a single condition that returns nil when sections is empty
and previousSummary is also empty. Preserve the existing behavior for nonempty
sections or a nonempty previousSummary.
- Around line 159-169: Consolidate the duplicated UTF-8-safe truncation logic
from clipBytes and capTaskEvidence into one shared byte-limit helper, retaining
trimming, ellipsis handling, and rune-boundary safety. Guard the reserved
ellipsis calculation so limits below three cannot produce a negative slice
index. Update capTaskEvidence to reuse clipBytes with maxTaskEvidenceBytes and
remove its duplicated truncation loop.

In `@internal/agent/task_state_test.go`:
- Around line 145-151: Extend the test around taskStateEventToolResult to add a
read_file failure for a.go, followed by a successful read_file result for b.go
with different arguments. Assert that the unresolved failure for a.go remains
while the b.go failure is resolved, covering argument identity in the failure
key and guarding the behavior in task state failure tracking.

In `@internal/agent/task_state.go`:
- Around line 325-360: Update appendBoundedUnique and removeTaskFailure to
allocate fresh result slices instead of filtering through values[:0], preserving
the existing ordering and bounded behavior. Also replace the same in-place
filtering pattern in mergeTaskFailures with newly allocated slices so stored or
parsed state is never mutated through shared backing arrays.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b3c138c-5881-4657-b4ff-accb0f6f9203

📥 Commits

Reviewing files that changed from the base of the PR and between cd0eb19 and 08a1c31.

📒 Files selected for processing (10)
  • internal/agent/compaction.go
  • internal/agent/compaction_metadata_test.go
  • internal/agent/compaction_preserve.go
  • internal/agent/compaction_preserve_test.go
  • internal/agent/compaction_projection.go
  • internal/agent/compaction_projection_test.go
  • internal/agent/compaction_test.go
  • internal/agent/loop.go
  • internal/agent/task_state.go
  • internal/agent/task_state_test.go

Comment thread internal/agent/compaction_preserve.go
Comment thread internal/agent/loop.go
Comment thread internal/agent/task_state.go
Comment thread internal/agent/task_state.go
@anandh8x
anandh8x marked this pull request as ready for review August 6, 2026 18:45
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: e45003399d2d
Changed files (17): internal/agent/compaction.go, internal/agent/compaction_metadata_test.go, internal/agent/compaction_preserve.go, internal/agent/compaction_preserve_test.go, internal/agent/compaction_projection.go, internal/agent/compaction_projection_test.go, internal/agent/compaction_recent_edits_test.go, internal/agent/compaction_test.go, internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/task_state.go, internal/agent/task_state_test.go, and 5 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@anandh8x

anandh8x commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai please review the previously skipped or unchecked areas on the latest head, including the remaining OnPermission thread. The parallel callback path is serialized by callbackMutex, and all current CI checks are green. If there are no remaining actionable issues, please approve the PR and clear the existing changes-requested verdict.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata and review state =='
gh pr view 873 --repo Gitlawb/zero --json headRefOid,reviewDecision,reviews,comments

echo '== Changed files and diff =='
git diff --stat HEAD^ HEAD
git diff -- internal/agent/loop.go internal/agent/task_state.go internal/agent/compaction.go internal/agent/compaction_projection.go internal/agent/compaction_preserve.go

echo '== Relevant declarations =='
ast-grep outline internal/agent/loop.go --items all --match 'Run|OnPermission|callbackMutex'
ast-grep outline internal/agent/task_state.go --items all --match 'TaskState|ObservePermission|Snapshot|ObserveTool'
ast-grep outline internal/agent/compaction_projection.go --items all
ast-grep outline internal/agent/compaction_preserve.go --items all

echo '== Synchronization and callback call sites =='
rg -n -C 6 --type go 'callbackMutex|OnPermission|ObservePermission|ObserveTool(Result)?|taskState' internal/agent

echo '== Test coverage related to permission and state =='
rg -n -C 4 --type go 'OnPermission|ObservePermission|callbackMutex|TaskStateRecordsDurableRuntimeEvidence|RepeatedCompaction' internal/agent

echo '== Open CodeRabbit review threads/comments =='
gh api repos/Gitlawb/zero/pulls/873/comments --paginate \
  --jq '.[] | select(.user.login == "coderabbitai") | {id, path, line, side, body, created_at}'

Length of output: 195155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='Gitlawb/zero'
pr=873

echo '== PR base and exact changed files =='
gh pr view "$pr" --repo "$repo" --json baseRefOid,headRefOid,files
base=$(gh pr view "$pr" --repo "$repo" --json baseRefOid --jq '.baseRefOid')
head=$(gh pr view "$pr" --repo "$repo" --json headRefOid --jq '.headRefOid')
printf 'base=%s\nhead=%s\n' "$base" "$head"
git diff --stat "$base" "$head"
git diff --check "$base" "$head"
git diff "$base" "$head" -- \
  internal/agent/loop.go \
  internal/agent/parallel_tools.go \
  internal/agent/task_state.go \
  internal/agent/compaction.go \
  internal/agent/compaction_projection.go \
  internal/agent/compaction_preserve.go \
  internal/agent/compaction_projection_test.go \
  internal/agent/compaction_preserve_test.go \
  internal/agent/task_state_test.go

echo '== Parallel batch lifecycle and callback wrappers =='
sed -n '145,245p' internal/agent/parallel_tools.go
sed -n '180,205p' internal/agent/loop.go
sed -n '1390,1440p' internal/agent/loop.go
sed -n '2348,2492p' internal/agent/loop.go

echo '== Task-state locking and snapshot mutation paths =='
sed -n '115,225p' internal/agent/task_state.go
sed -n '230,375p' internal/agent/task_state.go
sed -n '420,450p' internal/agent/task_state.go

echo '== Unresolved review threads at the current PR head =='
gh api graphql -F owner='Gitlawb' -F name='zero' -F number="$pr" -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          isResolved
          path
          line
          comments(first: 20) {
            nodes { author { login } body url }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[]
  | select(.isResolved == false)
  | {path, line, comments: [.comments.nodes[] | select(.author.login == "coderabbitai") | {body, url}]}'

Length of output: 62968


@anandh8x I reviewed #873 at 20c777d91a1e66a688008f9a2e4e2917c517f65e.

I found no remaining actionable issue.

The OnPermission callback is serialized by callbackMutex in executeParallelReadBatch. The batch waits for all worker goroutines before it returns to the main loop. The task.observe(...) call is therefore not concurrent with another permission callback or with subsequent main-loop task-state updates.

The remaining OnPermission thread does not apply to the current head. The exact PR diff also passes git diff --check.

I approve the PR and clear the existing review findings.

[resolve]
[approve]

You are interacting with an AI system.

@anandh8x

anandh8x commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai you confirmed there are no remaining actionable issues and stated that you approve, but GitHub still shows your original CHANGES_REQUESTED review and the old thread remains unresolved. Please submit the formal approval and clear the stale requested-changes verdict now.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@anandh8x I confirm that PR #873 has no remaining actionable issues.

I submit the formal approval and resolve the stale CodeRabbit review thread now.

✅ Action performed

Comments resolved and changes approved.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Request changes — one blocker, everything else is strong

The architecture is right and the measurement is honest work: projecting the middle to a bounded semantic transcript instead of shipping 617KB to a summarizer that returns 193 tokens either way is clearly correct, and the fail-open path (if len(summaryInput) == 0 { summaryInput = middle }) is exactly the right shape for a projection that could over-trim. I checked that out and ran it rather than reading it; go test ./internal/agent/ is green.

Blocker: isLikelyToolError drops most real failures, including Zero's own

The PR's contract says it retains "concise failures" and "unresolved failures". It largely does not, because isLikelyToolError matches only five exact prefixes (error:, tool error:, failed:, permission denied, command failed) and anything unmatched is treated as success and discarded entirely.

I ran it against real error strings:

"Error: Invalid arguments for view_image"          kept=true
"Error viewing shot.png: file is too large"        kept=false   <-- zero's own tool error
"--- FAIL: TestFoo (0.01s)"                        kept=false   <-- go test
"exit status 1"                                    kept=false
"panic: runtime error: index out of range"         kept=false
"fatal: not a git repository"                      kept=false   <-- git
"The model kimi-k2.6 does not exist"               kept=false   <-- provider
"read_file: no such file or directory"             kept=false

The first kept=false is the one that decides it. grep -rh 'errorResult("' internal/tools/ shows this repo emits "Error applying patch: " and "Error editing " — the Error <verb> shape, not Error: — so Zero's own edit and patch failures are dropped as if they had succeeded. A summary that silently loses "the patch did not apply" is worse than a long one, and the loss is invisible: nothing downstream can tell a dropped error from a tool that worked.

Two ways out, your call:

  • Cheap: widen the matcher and invert the default. Keep a tool result when it looks like an error or is short — a retained non-error costs a few dozen bytes, a dropped error costs the summary its most important content. Right now the tie goes the expensive way.
  • Proper: plumb the structural signal. tools.Result already knows StatusError; zeroruntime.Message has no error field, which is why prose matching is the only option here — so this is a real constraint you worked within, not an oversight. But matching prose for a decision this consequential is the shape AGENTS.md invariant 2 warns about, and a IsError bool on the tool message would end it permanently.

There is also no test asserting an error survives projection — compaction_projection_test.go covers user/assistant/tool-call shaping but nothing pins the [tool_error] path, which is why this gap is invisible to CI.

What I checked and found correct

  • Fail-open: an empty projection falls back to the original middle, so an over-aggressive projection degrades to today's behaviour instead of summarising nothing.
  • Previous summary carried separately rather than re-summarised, so repeated compaction does not telescope older decisions into noise.
  • Per-turn tool-call cap keeps the LAST N and says how many were omitted — the right end to keep, since the recent calls are the ones the summary needs.
  • Ordering and budgets are deterministic, so the same transcript projects to the same brief.

One caveat on the benchmark, not a blocker

The 99.75% reduction is real and I do not doubt it. What the numbers do not establish is summary quality: both versions produce ~193 tokens, and the question is whether the 193 tokens produced from a 1,435-byte brief are as good as the 193 from 617KB. The replay assertion checks that goal, decision, failure, plan and tail survive on one frozen corpus — worth saying plainly in the PR that this is a single-corpus structural check rather than a quality comparison across varied sessions, so nobody reads the table as proving more than it does.

Fix the error heuristic and I would approve this.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 7, 2026
gnanam1990
gnanam1990 previously approved these changes Aug 7, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Approve — the blocker is fixed, and fixed the right way

Re-reviewed at b01dba28. You took the structural option rather than widening the prose matcher, which is the one that actually ends this class of bug.

The chain is complete and I traced all three links:

  • zeroruntime.Message gains IsError bool — the field whose absence was the reason prose matching existed at all
  • loop.go sets it from toolResult.Status == tools.StatusError, i.e. from the same authority the tool itself reported
  • the projection reads !message.IsError && !isLikelyToolError(...), keeping the text check only as a fallback for session history written before the field existed — which is the right call, since old transcripts on disk have no flag to read

Both tool-result construction sites are covered. grep MessageRoleTool in internal/agent finds exactly two (loop.go:722 and :3286) and both set IsError, so there is no third path quietly producing an unflagged error message.

Verified against the exact strings from my last review, now carried as structured errors — every one survives:

"Error viewing shot.png: file is too large"   kept=true   <-- zero's own tool error
"--- FAIL: TestFoo (0.01s)"                   kept=true
"exit status 1"                               kept=true
"panic: runtime error: index out of range"    kept=true
"fatal: not a git repository"                 kept=true
"The model kimi-k2.6 does not exist"          kept=true

And the projection still projects: a 500-line successful read_file result is dropped, so the 99.75% reduction the PR is for is intact. go test ./internal/agent/ ./internal/zeroruntime/ green.

Everything I approved of last time still holds — the fail-open fallback to middle, the previous summary carried separately, keeping the last N tool calls rather than the first.

My earlier caveat about the benchmark stands unchanged and is still not a blocker: the table measures input size, not summary quality, and the replay assertion is a structural check on one frozen corpus. Worth a sentence in the PR description so the numbers are not read as proving more than they do.

Nice fix — the version that plumbs the status is strictly better than the one I suggested as the cheap option.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes. The direction is right and the benchmark is real, but the projection drops several things that are not reconstructible, and one claim in the description does not hold.

ask_user answers are discarded. executeAskUser returns Status: tools.StatusOK (loop.go:2039), so IsError is false, so the tool branch at compaction_projection.go:70 drops the body. That body is the user's own typed answer. If the user says "Postgres only, the MySQL branch is dead, do not touch it", that sentence is gone after compaction: the summarizer never sees it, and explicitConstraintsFromMessages cannot rescue it because that only scans user-role messages. The question is lost too, since compactionToolCallLine mines only file_path/path/query/pattern and cmd/command/script/shell, so an ask_user call renders as a bare * ask_user. This is the one I would fix first. It is the opposite of reconstructible.

/compact never reaches any of this. The description says the behaviour applies to "automatic context-pressure compaction and manual /compact", and lists manual /compact as validation. projectCompactionInput has exactly one call site, compaction.go:219, and nothing outside internal/agent reaches Compact/CompactMessages. The TUI path goes handleCompactCommand to compactActiveSession to PlanCompaction, a separate summarizer. Either wire it up or drop the claim, but as written the manual verification exercised code this PR does not touch.

apply_patch leaves no trace. Its file path lives only in the patch argument, which is not in the allowlist, and recentEdits matches only write_file/edit_file (compaction_preserve.go:119). So after compaction there is no record of which files were patched, in the brief or in preserved state. The same allowlist gap blanks web_fetch's URL and task's prompt.

The 120-line cap drops the oldest content, which is the part the summary exists to carry. capCompactionBrief keeps the last 120 lines, and the cap binds on essentially every real compaction, so the summarizer input sits around 2 KB regardless of whether the history is 24k or 1.3M tokens. Structured state does survive, so plan, edits, skills, instructions, approvals and the original run prompt are fine. What is never summarized even once is the older action record and assistant reasoning. Pre-PR the whole middle reached the summarizer and summarizeWithFallback halved it recursively when oversized; that path is now unreachable because the projection always returns exactly one message and the fallback bails at len(messages) < 2.

Two smaller ones. ResolvedFailureKeys is never cleared, so once a command has succeeded, any later failure of that same command is filtered out of preserved state for the rest of the run: the fail, fix, change, fail-again shape reports a green suite. And the carried-forward previous summary is clipped to 400 informative words each round, keeping the head, so the newest material in it decays away over repeated compaction.

To be clear about what holds up: the preservation machinery genuinely works. I checked objective, plan, skills, tool schemas and project instructions surviving repeated compaction, and the fourteen tests are real tests, not shape checks. Checking your three unrelated failures against the untouched baseline before claiming they were unrelated is exactly right.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@anandh8x that review is long, so here is the order I would take them in.

ask_user first. It is the only one where the lost content is the user's own words and nothing can regenerate it, and the fix looks small: that result simply is not reconstructible, so it should not sit in the reconstructible bucket.

The /compact claim second, because it is a decision rather than a fix. Either wire the TUI path through the projection or drop the line from the description. I would not hold the PR for the wiring if you would rather do it separately, but the description should not claim it meanwhile, and the manual validation should not be cited for a path this does not touch.

The rest can follow. apply_patch and the argument allowlist are mechanical, ResolvedFailureKeys is close to a one-line clear, and the 120-line cap is the one actually worth thinking about rather than patching quickly, since the right answer might be a byte budget rather than a line count.

Worth saying plainly: the preservation machinery holds up. I checked objective, plan, skills, tool schemas and project instructions surviving repeated compaction and they all do, and your fourteen tests are real tests rather than shape checks.

Also, my own first finding was wrong and I nearly sent it. I probed isLikelyToolError in isolation, watched it miss nine of eleven real failure strings, and was ready to call it blocking before checking the call site: IsError is consulted first, and exec_command.go sets StatusError on any nonzero exit, so failing tests and compile errors are preserved. The structured-status approach is sound. My measurement was the bad part.

@anandh8x
anandh8x dismissed stale reviews from gnanam1990 and coderabbitai[bot] via 8b45d99 August 8, 2026 04:45
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 8, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed 8b45d995 ("Unify manual and automatic compaction"), which landed after my approval and dismissed it. This is a much larger commit than the previous two — +467/-77 across 14 files, reaching into internal/sessions and internal/tui for the first time in this PR.

The core of it is right, and it is the fix I'd have asked for if I'd seen the manual path first. /compact used to build its summary from shapedPayloadPreview, which is an allow-listEventToolCall kept only id/name/toolName, EventToolResult only those plus status. The arguments and output fields never survived. So the manual summarizer was being handed a list of tool names and statuses and asked to write a useful summary of what happened. CompactionMessages + SummarizeCompactionMessages puts the real content through the same projection the automatic path uses, and TestManualCompaction... pins the join end-to-end by asserting the request carries agent.CompactionSummaryInstructions and that the ask_user question and answer both survive. Good test — it asserts the wire content, not struct fields.

I checked the obvious worry and it is not a problem: reading output/content/arguments raw out of the event payload is not a redaction bypass. Redaction already happened at the tool chokepoint before the event was written — loop.go scrubs result.Output and records result.Redacted, and tui/model.go:5439 persists that already-scrubbed value. See the doc note below, though.

One thing I'd fix before merge

plan.Truncated is derived by string-matching a marker that lives in another packageinternal/tui/session_controls.go:923:

truncated = truncated || strings.Contains(message.Content, "omitted to fit compaction budget") ||
    strings.Contains(message.Content, "[middle omitted]")

Both literals are inline strings in internal/agent/compaction_projection.go:104 and :207. Two packages, two copies of one fact, nothing asserting they agree — invariant #5.

I proved the drift is undetected. Rewording only the agent-side marker, exactly as a future refactor would:

compaction_projection.go:104  "...[transcript trimmed to fit the compaction budget]..."
compaction_projection.go:207  "\n...[trimmed]...\n"

go test ./internal/tui/ -count=1ok, whole suite green. plan.Truncated is now permanently false, and since RecordCompaction persists it (replay.go:223), the durable session record claims nothing was dropped when material was. The new TestStoreCanPlanCompactionWithoutBuildingLegacyPrompt only asserts !plan.Truncated on a three-event session, so nothing covers the true direction. It fails the other way too: a compacted message whose text happens to contain [middle omitted] flips the flag on.

The fix is the same move you already made elsewhere in this PR. Replacing isLikelyToolError's prose sniffing with the structural IsError field was exactly right; this is the same prose-sniffing pattern one layer up. Have SummarizeCompactionMessages return whether the projection truncated — it is the only code that knows — instead of leaving the caller to recognise its output by sight.

Smaller notes, none blocking

The event log is now read twice per /compact. PlanCompaction calls store.ReadEvents at replay.go:151, then compactActiveSession calls it again at session_controls.go:859 to resolve the same refs. Long sessions pay double I/O and double JSON parse for one compaction. Returning the events from PlanCompaction, or a variant that does, would avoid it. sessionEventsForRefs itself is good — checking Sequence and Type alongside the ID makes it a real clash guard rather than a lookup, and failing with "retry /compact" is the right call.

The two new boundary clippers lack the guard you just added to clipBytes. This commit hardened clipBytes for limit <= 0 and limit <= 3, but clipPrefixAtBoundary and clipSuffixAtBoundary right below it take a negative limit straight into text[:end], which panics. Unreachable today — both callers derive the limit from constants — so this is purely about the next person who lowers compactionBriefMaxBytes. Worth matching the hardening you applied twenty lines up.

PromptChars quietly changed meaning. It used to be len(prompt) for the whole built prompt; it is now the sum of len(message.Content) across projected messages, which excludes the system instruction that StreamCompletion actually sends. Small, but it is a recorded metric, so a comment saying which one it counts would keep later comparisons honest.

The projection budget grew a lot, and the PR title says the opposite. compactionBriefMaxLines = 120 became compactionBriefMaxBytes = 24KB, compactionPreviousSummaryWords = 400 became compactionPreviousSummaryBytes = 16KB, and mustPreserve now admits every successful mutating tool result rather than only errors. Against a raw transcript this is still a large reduction, so I don't think it's wrong — the changed-file evidence is the point of the PR. But whatever numbers the description quotes for "reduce compaction context" were measured before this commit. Worth re-measuring so the claim matches the code.

CompactionMessages' doc comment is inaccurate in a security-relevant way. It says "security-sensitive event payloads use the same redacted previews as the session compaction plan." Only the default branch and ask_user_answers do; the three shaped branches read payload fields directly. As above that is safe, because redaction happened upstream at the tool chokepoint — but the comment states the wrong reason, and someone adding a fourth shaped branch would reasonably conclude they inherit redaction from this function and skip checking their source. Please state the invariant it actually relies on.

Verified

  • go test ./internal/agent/ ./internal/sessions/ ./internal/tui/ ./internal/zeroruntime/ -count=1 — all green at 8b45d995.
  • Mutation-checked the marker coupling as described above; tree restored clean afterward.
  • Traced ChangedFiles from tools.Result through loop.go:723 and copyMessages into recentEdits. Checked the ordering concern raised by the second for loop over messages — it is not a defect, because write_file/edit_file set ChangedFiles too, so their paths reappear in the second pass and lastSeenOrder still lands everything chronologically. The first loop is now near-redundant except for pathByID and failed edits.
  • removeString in task_state.go:245 is a genuine fix: a recurring failure previously stayed in ResolvedFailureKeys while also sitting in UnresolvedFailures.

Not blocking on any of this — today's behavior is correct in every case above. The truncation-marker coupling is the one I'd want closed before merge, since it is cheap and it is a named recurring defect class in this repo. Happy to re-approve as soon as that's addressed.

gnanam1990
gnanam1990 previously approved these changes Aug 8, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving.

To put a formal verdict on the review above: nothing in 8b45d995 is wrong today. The redaction question I chased turned out clean, the ordering concern in recentEdits dissolved on inspection, removeString is a real fix, and all four affected packages test green. The commit repairs a manual /compact that was genuinely broken — summarizing from an allow-listed preview that had dropped every arguments and output field — and it does so by reusing the automatic path rather than growing a second one. That is the right shape.

The one item I'd still like closed is the truncation marker: plan.Truncated is recognised by string-matching literals that live in internal/agent, and I showed that rewording them leaves the whole tui suite green while the persisted session record silently starts claiming nothing was dropped. It is a latent-drift risk rather than a live bug, which is why it isn't blocking — but it is invariant #5, it is cheap, and SummarizeCompactionMessages returning the fact is strictly less code than sniffing for it downstream. Please take it either in this PR or as an immediate follow-up; if you'd rather not grow this commit further, a follow-up issue is fine by me.

The four smaller notes — the double ReadEvents, the missing limit <= 0 guard on the two new boundary clippers, the changed meaning of PromptChars, and the stale "reduce compaction context" numbers in the description — are all optional. Only the description one has any reader-facing consequence, so it's worth a quick re-measure before merge.

Nice work on the IsError field earlier in this PR, incidentally — that was the structural fix rather than the cheap one, and this commit is better for having it to build on.

@anandh8x
anandh8x dismissed stale reviews from gnanam1990 and coderabbitai[bot] via fe11979 August 8, 2026 05:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agent/compaction.go`:
- Around line 220-224: Update CompactMessages around SummarizeCompactionMessages
to propagate summaryResult.Truncated and summaryResult.ProjectedChars into the
returned CompactionResult instead of retaining only SummaryText. Add a
regression test exercising CompactMessages with a truncated projection and
asserting both metadata fields, including the non-truncated behavior as
appropriate.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c390003f-d322-4a96-980f-2c50065cdd85

📥 Commits

Reviewing files that changed from the base of the PR and between 8b45d99 and fe11979.

📒 Files selected for processing (6)
  • internal/agent/compaction.go
  • internal/agent/compaction_projection.go
  • internal/agent/compaction_projection_test.go
  • internal/sessions/replay.go
  • internal/tui/session_controls.go
  • internal/tui/session_controls_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/tui/session_controls_test.go
  • internal/tui/session_controls.go
  • internal/agent/compaction_projection.go
  • internal/sessions/replay.go

Comment thread internal/agent/compaction.go
gnanam1990
gnanam1990 previously approved these changes Aug 8, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at fe119799. My previous approval was dismissed by this push, which crossed with it by about three minutes.

You took all four notes, and the truncation fix is better than what I proposed. I suggested SummarizeCompactionMessages return the fact; you threaded it properly — compactionProjection{messages, truncated} out of the projection, CompactionSummaryResult{SummaryText, ProjectedChars, Truncated} out of the summarizer — so internal/tui no longer recognises agent's output by sight. grep for either marker in internal/tui now returns nothing.

It also catches a case the string sniffing would only have caught by luck: len(fullSummary) > compactionPreviousSummaryBytes reports truncation of the previous summary independently of the brief. The old sniff would have matched [middle omitted] from clipHeadTailBytes incidentally, and silently stopped the day that marker was reworded.

Re-ran the mutation that proved the gap last time. Flipping capCompactionBrief's new return to false:

--- FAIL: TestManualCompactionPersistsStructuralTruncationMetadata
    session_controls_test.go:589: compaction did not persist structural truncation

Same edit left the entire tui suite green at 8b45d995. The cross-package join is now pinned, which was the whole ask.

The other three are done too:

  • limit <= 0 guards on both clipPrefixAtBoundary and clipSuffixAtBoundary, matching clipBytes.
  • PromptChars is both documented and more honest than before — len(CompactionSummaryInstructions) + ProjectedChars, and compactionMessageChars counts tool call names and arguments rather than Content alone. The test pinning the exact value keeps it that way.
  • The CompactionMessages comment now names the invariant it actually depends on: that the session writer scrubs tool output before persistence, with ask_user answers and generic previews redacted locally. That is the correct statement, and it tells the next person adding a shaped branch what they need to check.

Verified at fe119799: go test ./internal/agent/ ./internal/sessions/ ./internal/tui/ -count=1 all green; mutation as above; working tree clean afterward.

Left open, both optional and neither worth another round: the session event log is still read twice per /compact (PlanCompaction at replay.go:151, then again at session_controls.go:859), and the PR description's "reduce compaction context" figures predate the budget changes in 8b45d995. The second is worth a quick edit to the description before merge so the claim matches the constants, but it is not a code change.

Good sequence of fixes on this PR — IsError, then the retry wording, and now this. Each one took the structural option over the cheap one.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Aug 8, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Everything I raised is addressed, and I re-checked each one against the code rather than the commit message.

Both blocking ones are fixed and I proved them: an ask_user answer and an apply_patch file path both survive the projection now (ask_user ANSWER survives: true, apply_patch FILE survives: true). The mustPreserve gate on ask_user and ChangedFiles is the right shape, since those are exactly the two categories that are not reconstructible.

The /compact claim is now true rather than dropped, which is the better of the two options I offered. SummarizeCompactionMessages applies the shared projection and both paths go through it, so the description matches the code. Only the event-to-message conversion is separate, and it carries IsError and ChangedFiles through, with ask_user_answers handled explicitly and redacted.

capCompactionBrief keeping both edges under a byte budget is a better answer than the line cap I complained about, and the comment says why: the oldest material has not necessarily been summarized before. The argument allowlist growing to url, prompt, description and question closes the web_fetch and task gaps too. ResolvedFailureKeys is now cleared on re-failure, so the fail-fix-fail shape reports honestly.

The split-fallback point I raised is effectively moot now rather than fixed: the projection still returns a single message, but the byte cap bounds the input up front, so the oversized-middle case it protected against cannot arise the same way. Worth knowing rather than acting on.

One thing that is not yours: TestEagerToolSchemaTokenBudget fails on this branch at 3588 tokens against a 3550 ceiling. It is pre-existing. I bisected it to cd0eb19 (#843, which added view_image to the eager set); 4ca4fa75 and edf660ab both pass, and current main fails at 3578. So this branch inherits it and adds about 10 tokens. I am raising it separately and it is not a reason to hold this PR.

@anandh8x
anandh8x dismissed stale reviews from Vasanthdev2004 and gnanam1990 via e450033 August 8, 2026 05:43

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at e4500339. Third push in forty minutes; each one dismissed the prior approval, so this is a re-approve of the same PR at a newer head rather than a new objection.

e4500339 is small and symmetric: CompactionResult now carries ProjectedChars and Truncated, so the automatic path reports the same two facts SummarizeCompactionMessages already hands the manual path. Before this, the projection computed them and the automatic caller dropped them on the floor. The test covers both directions — exact compactionMessageChars agreement with a non-zero value on the compacting path, and zero values on the no-op path so the fields cannot quietly report stale numbers when nothing was compacted.

One observation, not a request. Nothing in production reads CompactionResult.ProjectedChars or CompactionResult.Truncated yet — grep finds only the two assignments in compaction.go:248-249 and the assertions in compaction_metadata_test.go. The session_controls.go:874 reader is CompactionSummaryResult on the manual path, a different struct. So automatic compaction can still truncate without anything recording it or telling the user, which is the same gap /compact just closed. Surfacing the fields is the right first half and the struct is exported, so this is fine as-is — I mention it only so the second half doesn't get forgotten, since the metadata now exists and costs nothing to record.

Verified at e4500339: go test ./internal/agent/ ./internal/sessions/ ./internal/tui/ ./internal/zeroruntime/ -count=1 all green.

Everything from my fe119799 review still stands — the structural truncation reporting, the clipper guards, the PromptChars accounting, and the corrected CompactionMessages doc comment. The two open items remain optional and unchanged: the doubled ReadEvents per /compact, and the PR description's pre-8b45d995 figures for "reduce compaction context", which are worth correcting before merge so the stated claim matches the constants.

Note that the PR still shows blocked on @Vasanthdev2004's changes-requested from yesterday 18:12, which predates all three of these commits.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/agent/compaction_metadata_test.go (1)

137-142: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that the captured projection is actually bounded.

The test checks Truncated and metadata propagation, but it does not prove that projection removed content. A regression could pass by sending the full oversized middle to the summarizer and setting Truncated to true.

Compare the captured character count with the original middle input or the production projection limit.

Proposed assertion
  if !result.Compacted || !result.Truncated {
    t.Fatalf("compaction metadata = compacted %t, truncated %t; want both true", result.Compacted, result.Truncated)
  }
+ middleChars := compactionMessageChars(messages[1 : len(messages)-2])
  if result.ProjectedChars != compactionMessageChars(captured) || result.ProjectedChars == 0 {
    t.Fatalf("ProjectedChars = %d, want %d", result.ProjectedChars, compactionMessageChars(captured))
  }
+ if result.ProjectedChars >= middleChars {
+   t.Fatalf("projection was not bounded: got %d characters from %d", result.ProjectedChars, middleChars)
+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/compaction_metadata_test.go` around lines 137 - 142, Extend
the assertions in the compaction metadata test around result.ProjectedChars and
captured to verify the captured projection is smaller than the original
oversized middle input, or otherwise does not exceed the production projection
limit. Preserve the existing checks for Compacted, Truncated, nonzero
ProjectedChars, and metadata propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/agent/compaction_metadata_test.go`:
- Around line 137-142: Extend the assertions in the compaction metadata test
around result.ProjectedChars and captured to verify the captured projection is
smaller than the original oversized middle input, or otherwise does not exceed
the production projection limit. Preserve the existing checks for Compacted,
Truncated, nonzero ProjectedChars, and metadata propagation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 634e8056-d9e4-4f62-b567-ff6ecc87c87c

📥 Commits

Reviewing files that changed from the base of the PR and between fe11979 and e450033.

📒 Files selected for processing (2)
  • internal/agent/compaction.go
  • internal/agent/compaction_metadata_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/agent/compaction.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving. My earlier approval was dismissed when you pushed, so I read the two new commits rather than just re-stamping it.

fe119799 and e4500339 are an improvement on what I approved. Returning compactionProjection{messages, truncated} and having capCompactionBrief report (string, bool) replaces the TUI sniffing its own summariser output for the literal "omitted to fit compaction budget". I checked and that string match is gone from session_controls.go. Truncation of the carried-forward previous summary is now tracked too, which it was not before, and clipHeadTailBytes picked up a limit <= 0 guard.

Every compaction test passes on this head.

Two failures in the suite, both pre-existing and neither yours. TestEagerToolSchemaTokenBudget is the eager-schema ceiling: this branch reads 3588 because it predates #867's trim to 3578, and #877 raises the ceiling to fix main. TestAltScreenTranscriptScrollKeepsFooterFixed fails on pristine main too; I confirmed that separately against a clean worktree earlier today.

Worth rebasing on main once #877 lands so your CI goes green.

@anandh8x
anandh8x merged commit ff608c7 into main Aug 8, 2026
9 checks passed
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.

3 participants