fix(agent,memory): refuse session JSON truncation that wipes history - #70
Conversation
When serialized session JSON exceeded SESSION_JSON_MAX, append and load silently clipped mid-JSON. The next agent_run parse then treated history as empty. Refuse oversize copies instead of slicing. Refs: #70
548d219 to
8d8748c
Compare
There was a problem hiding this comment.
Stale comment
Signals are clean: Cursor Bugbot and Cursor Security Agent both passed with no findings that need human review. GitHub will not accept an approval from this automation on a PR it authored, so this is a non-blocking comment only. adriannoes is already requested as reviewer; no additional reviewers were assigned.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Summary
This is a correct fail-closed fix for a real history wipe. When serialized session JSON went over SESSION_JSON_MAX (128 KiB), append_exchange_to_session_json and compact_session_via_llm clipped with len = size - 1, session_save stored invalid JSON, the next cJSON_Parse treated history as empty, and the following persist wrote a fresh array. session_load had the same clip. The PR refuses the copy instead, skips save, and adds a regression that reproduces the near-cap + fat-reply trigger. Matches the #61 pattern. No merge blockers.
Must Fix
None. Size checks use n >= cap then memcpy(..., n + 1), so the NUL fits. On refuse, printed is cJSON_freed and the caller buffer is left untouched. agent_persist_session already skips session_save when append returns non-zero. test_session_overflow_does_not_corrupt_history seeds ~124 KiB of valid JSON, forces a 16 KiB reply, then asserts the stored blob still parses and the next agent_run still sees the marker. test_session_load_rejects_oversized covers the small-buffer load path. I ran test_memory locally (all passed). test_agent could not be linked in this image (router.c needs libcurl headers). GitHub static, test, release was still pending at review time.
Should Fix
- Silent persist skip.
agent_runstill returns 0 when append/compact refuse, with no log. Channel dispatch uses a 32 KiB response buffer; ASAPtask.requestuses 256 KiB. A single ASAP reply cannot fit in the 128 KiB session blob, even on an empty session, so that turn is never written. Log session id andlenvs cap when refusing. session_loadoverflow is indistinguishable from a missing row. It zeros the out buffer, then returns-1.agent_prepare_context(around line 431) ignores the rc.session_savestill has no cap. A valid oversized row (not produced by current agent writers) would look empty and be overwritten by the next small persist. Distinguish overflow from not-found, or skip persist when load failed for a present row.- Compaction is message-count only (
msg_count > max_ctx). The new overflow test setsmax_context_messages = 40with two huge messages, so compact never runs. Near-cap sessions with few large messages will freeze: later turns keep skipping save. Size-based drop-oldest (or compact-until-it-fits) is the real product follow-up; out of this PR's integrity fix.
Nice to Have
- A compact-path overflow test (the
plen >= session_buf_sizebranch has no direct coverage). - Assert the fat reply (
RRR...) is absent from the saved blob, so a "raise the cap and save anyway" change cannot sneak through. TodayLOAD_CAP(130 KiB) already failssession_loadif a ~140 KiB blob were stored, which is a decent implicit check. - Reuse
spy_providerfor the second turn instead of cloning it askeep_history_provider. session_loadinmemory.hstill talks about the implementation allocatingmessages_out; it does not mention oversized-buffer rejection.
Positive Highlights
- Comments state why truncation is fatal (next parse wipes history), not just what the
ifdoes. - Both serialize sites were fixed, not only append. Load refuse matches.
- The agent regression is a real trigger (near-cap JSON + large reply), then a second turn that spies provider messages. That would have failed on the old clip-and-save path.
- CHANGELOG Unreleased records the behavior change.
Sent by Cursor Automation: Adrianno’s personal code review
…ized load Refuse copies now log session_id, len, and cap. session_load returns SESSION_LOAD_TOO_LARGE so a later small turn cannot overwrite the stored blob. Refs: #70
There was a problem hiding this comment.
Signals are clean: Cursor Bugbot and Cursor Security Agent both passed with no findings that need human review. GitHub will not accept an approval from this automation on a PR it authored, so this is a non-blocking comment only. adriannoes is already requested as reviewer; no additional reviewers were assigned.
Sent by Cursor Approval Agent: Pull Request Router and Approver


Summary
When serialized session JSON exceeded
SESSION_JSON_MAX(128 KiB),append_exchange_to_session_json/compact_session_via_llmsilently clipped mid-JSON and still returned success, sosession_savepersisted a corrupt payload. The nextagent_runfailed to parse the session and treated history as empty (permanent wipe on the following save).session_loadhad the same clip-into-invalid-JSON path.Rewritten on current
origin/development(includes #59 and #63). Do not cherry-pick the original Bot commit.Trigger
Near-cap session (~124 KiB of valid JSON) plus a fat assistant reply (session cap is 128 KiB). Reproduced with a regression test; pre-fix truncation yields unparseable JSON.
Fix
append_exchange_to_session_json/compact_session_via_llm: return -1 instead of truncating (skipsession_save)session_load: reject blobs that do not fit the caller buffer instead of clippingTest plan
make test_agent(includestest_session_overflow_does_not_corrupt_history)make test_memory(includestest_session_load_rejects_oversized)CI=true make testmake staticNote
Medium Risk
Changes session load/save semantics at the 128 KiB boundary—turns may not persist when over cap, but that avoids corrupt JSON and accidental history wipes in SQLite.
Overview
Fixes a data-loss bug where session history could be permanently wiped when serialized JSON hit the 128 KiB cap.
Load path:
session_loadno longer clips oversized blobs into invalid JSON. It returnsSESSION_LOAD_TOO_LARGEand leaves the output buffer empty so callers can tell “too big” apart from “missing.”Save path:
append_exchange_to_session_jsonand LLM compaction now usecopy_printed_session_json, which refuses mid-JSON truncation instead of saving corrupt payloads. If load failed withSESSION_LOAD_TOO_LARGE,agent_persist_sessionskipssession_saveso a later small turn cannot overwrite the oversized row with a fresh empty/history-less JSON.Regression coverage in
test_agent(overflow + oversize-load) andtest_memory(test_session_load_rejects_oversized); CHANGELOG updated.Reviewed by Cursor Bugbot for commit 1b54efd. Bugbot is set up for automated code reviews on this repo. Configure here.