From 6a1190296045bb10857b2bf35c3cd62f6efbf4b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 12:18:17 +0000 Subject: [PATCH] Keep compaction boundaries aligned with raw user text strip_compaction_preambles used rfind on the orchestrator QUERY wrapper and then sliced at the first blank line after any leading '['. User turns that embed that delimiter or start with a bracket block stored a truncated boundary, so remap after reload failed open (full history plus summary). Strip only known OPEN TODOS / KNOWN PITFALLS / INTENT envelopes and take the first QUERY wrapper so the persisted boundary matches the SQLite row. Co-authored-by: Tyler Reckart --- CHANGELOG.md | 8 +++++ src/context_compaction.cpp | 39 +++++++++++++++------ tests/test_context_compaction.cpp | 57 +++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dae12b43..9b4fa6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ loosely while pre-1.0 (breaking changes can land on minor bumps). ## [Unreleased] +### Fixed +- **Compaction boundary vs user `QUERY:` / `[` text.** + `strip_compaction_preambles` now takes the first orchestrator + `\n\nQUERY: ` wrapper (not the last) and strips only known + `[OPEN TODOS]` / `[KNOWN PITFALLS]` / `[INTENT]` envelopes. A user + turn that embeds that delimiter or starts with `[` no longer stores a + truncated boundary that fails remap after reload. + ## [0.13.4] — 2026-09-10 ### Fixed diff --git a/src/context_compaction.cpp b/src/context_compaction.cpp index 00015c0c..3ad0a7d5 100644 --- a/src/context_compaction.cpp +++ b/src/context_compaction.cpp @@ -108,28 +108,45 @@ std::string compaction_boundary_content(std::string_view content) { return std::string(content.substr(0, kCompactionBoundaryMax)); } -std::string strip_compaction_preambles(std::string_view content) { - std::string s(content); - // Drop [OPEN TODOS]…[END OPEN TODOS] blocks injected before the turn. +namespace { + +void erase_marked_block(std::string& s, std::string_view start_mark, + std::string_view end_mark) { for (;;) { - const auto start = s.find("[OPEN TODOS]"); + const auto start = s.find(start_mark); if (start == std::string::npos) break; - const auto end = s.find("[END OPEN TODOS]", start); + const auto end = s.find(end_mark, start); if (end == std::string::npos) break; - size_t erase_to = end + std::string_view("[END OPEN TODOS]").size(); + size_t erase_to = end + end_mark.size(); while (erase_to < s.size() && (s[erase_to] == '\n' || s[erase_to] == '\r')) ++erase_to; s.erase(start, erase_to - start); } - // Index master wraps the user line as "…\n\nQUERY: ". +} + +} // namespace + +std::string strip_compaction_preambles(std::string_view content) { + std::string s(content); + // Orchestrator injects these as whole marked blocks before the turn. + // Strip by start/end markers — not "leading '[' + first blank line" — + // so user text that starts with '[' or contains "\n\nQUERY: " survives. + erase_marked_block(s, "[OPEN TODOS]", "[END OPEN TODOS]"); + erase_marked_block(s, "[KNOWN PITFALLS", "[END KNOWN PITFALLS]"); + // Index master wraps the user line as "…\n\nQUERY: ". Use the + // first mark: a user who typed that same delimiter must keep it, or + // the stored boundary will not match the raw DB row and remap fails + // open (covered_until=0, full history + summary). static constexpr std::string_view kQuery = "\n\nQUERY: "; - const auto q = s.rfind(kQuery); + const auto q = s.find(kQuery); if (q != std::string::npos) s = s.substr(q + kQuery.size()); - // Known-pitfalls / lesson blocks end before the real user text; if a - // double newline remains after a leading bracket block, keep the tail. - if (!s.empty() && s.front() == '[') { + // Specialist ingress: "[INTENT] …\n\n" + user text. Only this known + // prefix — a user turn that begins with '[' is not a preamble. + static constexpr std::string_view kIntent = "[INTENT]"; + if (s.size() >= kIntent.size() && + s.compare(0, kIntent.size(), kIntent) == 0) { const auto sep = s.find("\n\n"); if (sep != std::string::npos) s = s.substr(sep + 2); } diff --git a/tests/test_context_compaction.cpp b/tests/test_context_compaction.cpp index 7382c5e0..d10bcb4c 100644 --- a/tests/test_context_compaction.cpp +++ b/tests/test_context_compaction.cpp @@ -212,6 +212,63 @@ TEST_CASE("strip_compaction_preambles removes todos and QUERY wrapper") { strip_compaction_preambles(raw)))); } +TEST_CASE("strip_compaction_preambles keeps user-embedded QUERY delimiter") { + // Index wrap uses the first "\n\nQUERY: ". rfind would keep only + // "docs" and fail to match the raw DB row on remap. + const std::string raw = + "AGENTS: none loaded\n" + "\n" + "QUERY: see\n\nQUERY: docs"; + CHECK(strip_compaction_preambles(raw) == "see\n\nQUERY: docs"); + CHECK(boundary_content_matches("see\n\nQUERY: docs", + compaction_boundary_content( + strip_compaction_preambles(raw)))); +} + +TEST_CASE("strip_compaction_preambles keeps user text that starts with '['") { + const std::string raw = + "[KNOWN PITFALLS — your prior lessons]\n" + " - [sig] body with a blank line\n" + "\n" + "still in the lesson (#1)\n" + "[END KNOWN PITFALLS]\n" + "\n" + "[NOTE]\n" + "\n" + "actual question"; + CHECK(strip_compaction_preambles(raw) == "[NOTE]\n\nactual question"); + CHECK(boundary_content_matches("[NOTE]\n\nactual question", + compaction_boundary_content( + strip_compaction_preambles(raw)))); +} + +TEST_CASE("strip_compaction_preambles drops specialist INTENT envelope only") { + const std::string raw = + "[INTENT] kind=implement conf=0.80 source=heuristic\n" + "GOAL: ship it\n" + "\n" + "[NOTE]\n" + "\n" + "keep this"; + CHECK(strip_compaction_preambles(raw) == "[NOTE]\n\nkeep this"); +} + +TEST_CASE("remap unique boundary survives user-embedded QUERY") { + auto hist = make_history(8); + hist[4].role = "user"; + hist[4].content = "see\n\nQUERY: docs"; + CompactionState st; + st.summary = "prior"; + st.covered_until = 99; + st.generation = 1; + st.boundary_role = "user"; + st.boundary_content = compaction_boundary_content( + strip_compaction_preambles( + "AGENTS: none loaded\n\nQUERY: see\n\nQUERY: docs")); + remap_compaction_onto_history(st, hist, /*keep=*/16); + CHECK(st.covered_until == 4); +} + TEST_CASE("remap_compaction_onto_history clears empty summary") { auto hist = make_history(20); CompactionState st;