Repository navigation
fix(agent): summary and compaction history correctness - #121
adriannoes wants to merge 5 commits into
Conversation
The kept tail starts at max_ctx-1, but the summary window stopped one message earlier, so that turn was deleted from the session and never summarized. Supersedes #102.
Compaction deleted every message before the kept tail even when the summary source filled up first. A later short line replaced the truncated long line, so that history was removed without being summarized. Supersedes #108.
There was a problem hiding this comment.
Code review — PR #121
Repo context: ShellClaw is a C99 edge AI agent (Jetson / Raspberry Pi) with a ReAct loop in src/core, providers, tools, channels, gateway, ASAP, sandbox, and hardware. Compaction lives in compact_session_via_llm and is meant to shrink SQLite session history when msg_count > max_context_messages.
What this PR delivers: Two history-loss fixes in the compaction path (superseding #102 and #108). (1) The summary window now includes index msg_count - max_ctx so the turn at the keep-tail boundary is not deleted without being summarized. (2) When a message does not fit in SUMMARY_SOURCE_MAX, compaction stops and keeps that message and everything after it instead of skipping the long line and deleting it anyway.
Scope: src/core/agent.c, tests/test_agent.c only (+135/−10).
Verification: Local CI=true make test_agent → test_agent: all tests passed. GitHub static, test, release was still pending at review time.
Summary
Solid, focused correctness fix. The off-by-one math matches keep = max_ctx - 1 (one summary + tail), summary_source_append is atomic (whole line or roll back), and both regressions are covered by tests. No merge blockers in the claimed #102/#108 fixes. Remaining notes are follow-up debt from the prefer-keep policy and changelog hygiene.
Must Fix
None.
Should Fix
-
Oversized-message compaction stall (
src/core/agent.c~132–140, ~169–171) — If one message cannot fit inSUMMARY_SOURCE_MAX(64 KiB):- First message too large →
used == 0→return -1→ session unchanged → every lateragent_runretries and fails (agent_prepare_contextignores the return code). - Later message too large → partial compact keeps from that index, so
new_countstays> max_ctx. Later turns only re-summarize what sits before the blocker (often just the prior summary) and never reachmax_ctx. Session growth can eventually hitSESSION_JSON_MAX/SESSION_LOAD_TOO_LARGE.
Prefer a truncate-into-summary stub (or drop-with-marker) for the single oversized line so compaction can make progress. Rare under the 128 KiB session cap, but sticky once it happens, and each turn still pays a summarize LLM call.
- First message too large →
-
CHANGELOG — Add Unreleased Fixed bullets for both history-loss fixes (#102 / #108). Same gap called out on recent shellclaw fix PRs (#119, #120).
Nice to Have
- Stale comment in
tests/test_agent.c(~388): it still says msg_10 was skipped by both windows; after the fix it is intentionally in the summary source. - Fit test only asserts marker + summary string; could also assert
alpha_smallleft the raw history and that a follow-up turn still cannot shrink past the 65 KiB line (documents the stall). next_unsummarized == 0is redundant withused == 0on the first-append failure path.
Positive Highlights
- Boundary math is correct — With 50 messages and
max_ctx = 40, summarize0..10, keep from11; test locksmsg_10in the summary source andmsg_11in the post-compact chat spy. - Atomic append + early break — Fixes the old “skip long line, accept a later short line, delete the long one” hole without mid-line truncation into the summarizer input.
- Fail closed when nothing fits — Returning
-1and leaving the session alone is the right call versus silent deletion.
Dimension notes
| Area | Notes |
|---|---|
| Correctness | Claimed fixes hold; edge case is the oversized-message stall above. |
| Architecture | Stays inside existing compact_session_via_llm; no layering issues. |
| Security | No new trust boundaries; summarizer still sees session text already stored for the agent. |
| Performance | Partial-compact path can re-invoke the summarizer every turn without shrinking (see Should Fix #1). |
| Tests | Both supersede bugs have regression coverage; make test_agent green locally. |
| Maintainability | Helper + comments are clear; CHANGELOG gap is the main polish miss. |
Sent by Cursor Automation: Code review
| int keep = max_ctx - 1; | ||
| int start = msg_count - keep; | ||
| int start = (next_unsummarized < to_summarize_n) | ||
| ? next_unsummarized : (msg_count - keep); |
There was a problem hiding this comment.
Should Fix: When next_unsummarized < to_summarize_n because a single line exceeds SUMMARY_SOURCE_MAX, start = next_unsummarized leaves summary + (msg_count - start) always above max_ctx. Later runs only re-summarize what sits before that blocker (often the previous summary) and never shrink the session. If the first message alone does not fit, the path returns -1 at ~137 and every agent_run retries with no progress (agent_prepare_context ignores the rc).
Prefer truncating that one oversized line into the summary source (with a clear omitted-bytes marker) or replacing it with a stub so compaction can advance. Prefer-keep over silent delete is right for #108; unbounded stall is the leftover debt.
| compaction_call_count++; | ||
| if (compaction_call_count == 1) { | ||
| const char *src = (message_count >= 2 && messages[1].content) ? messages[1].content : ""; | ||
| /* msg_10 is the index the summary window and the kept tail both skipped. */ |
There was a problem hiding this comment.
Nice to Have: This comment still says msg_10 was skipped by both the summary window and the kept tail. After the to_summarize_n = msg_count - max_ctx + 1 fix, msg_10 is intentionally in the summary source (that is what compaction_source_has_boundary asserts). Suggest rewording to “boundary index that the old window omitted.”
Compaction wrote the summarized transcript before the reply, so a failed provider call replaced the stored session with a summary the user never accepted. When the kept tail already starts with a user turn, fold the summary into that turn so Anthropic does not reject the session for consecutive user roles. Supersedes #126.
…he summary The prefer-keep policy made any single message larger than the 64 KiB summary source impossible to summarize: an oversized first line aborted compaction with an empty source, so the session never shrank again, and an oversized later line was kept raw together with everything after it, so new_count never dropped to max_ctx and the session could grow until SESSION_JSON_MAX / SESSION_LOAD_TOO_LARGE. Summarize such a line truncated with a [truncated] marker so the pass still consumes it and the message count goes down. Lines that fit a fresh source but lack room in the current pass keep the prefer-keep path, and whole-line append-or-rollback atomicity is unchanged for fitting lines. Also drop the next_unsummarized==0 abort clause that used==0 already covers (Should Fix 1, PR #121 review).
…ixes The PR #121 review flagged the missing Unreleased Fixed entries for the compaction work consolidated on this branch (boundary turn summarized, history kept when it cannot fit the summary, deferring the summary save behind the turn with the roles-alternating fold, and the new guarantee that one oversized message cannot stall compaction).
|
Review response — all items addressed. Must Fix: None raised. Should Fix
Nice to Have
Consolidation: Bugbot PR #126 ( Validation: |


Summary
Consolidates two cursor high-severity agent fixes (originally
cursor/high-severity-issues-5d10andcursor/high-severity-issues-104f) as operator-authored commits.Testing
make test_agent— passedCI=true make test— all 45 test binaries passedmake static— passedSupersedes #102 and #108.