Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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.
- **`/schedule resume` is paused-only.** The writ no longer flips
`completed`, `failed`, `canceled`, `running`, or already-`active` rows
back to `active`, and `/schedule pause` refuses those terminal
Expand Down
39 changes: 28 additions & 11 deletions src/context_compaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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: <text>".
}

} // 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: <text>". 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);
}
Expand Down
57 changes: 57 additions & 0 deletions tests/test_context_compaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down