From f4857bc0c6d4e4930f967482d31fa428189d890b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 00:14:12 +0000 Subject: [PATCH] Strip the first orchestrator QUERY marker on TUI transcript replay. replay_user_echo_text used rfind("\n\nQUERY: ") after requiring AGENTS in the head. Index turns always wrap once (global_status + QUERY + user text), so a user line that embeds that delimiter replayed as only the suffix after the last marker. Live echo showed the full line. Use find so the boundary matches the single orchestrator wrapper, same class as the compaction preamble strip. Co-authored-by: Tyler Reckart --- CHANGELOG.md | 5 +++++ src/repl/transcript_replay_predicates.cpp | 6 +++++- tests/test_transcript_replay_predicates.cpp | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dae12b43..6003b8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ loosely while pre-1.0 (breaking changes can land on minor bumps). ## [Unreleased] +### Fixed +- **TUI replay QUERY boundary.** `replay_user_echo_text` now strips the + first `\n\nQUERY: ` after the AGENTS roster (not the last). User text + that embeds that marker no longer replays as only the suffix after it. + ## [0.13.4] — 2026-09-10 ### Fixed diff --git a/src/repl/transcript_replay_predicates.cpp b/src/repl/transcript_replay_predicates.cpp index 0bc9cd15..2fdde45a 100644 --- a/src/repl/transcript_replay_predicates.cpp +++ b/src/repl/transcript_replay_predicates.cpp @@ -28,7 +28,11 @@ std::string_view replay_user_echo_text(const Message& m) { // AGENTS …\n\nQUERY: // for the master ("index") agent. Live UI echoes only . static constexpr std::string_view kQueryMark = "\n\nQUERY: "; - const auto q = content.rfind(kQueryMark); + // Orchestrator::send_streaming / send_internal emit exactly one + // "\n\nQUERY: " after global_status() (always starts with AGENTS). + // find — not rfind — so user text that itself contains that marker + // is not truncated to the suffix after the last occurrence. + const auto q = content.find(kQueryMark); if (q == std::string_view::npos) return content; const std::string_view head = content.substr(0, q); // Require an AGENTS roster line in the preamble so a user who literally diff --git a/tests/test_transcript_replay_predicates.cpp b/tests/test_transcript_replay_predicates.cpp index e200b9a5..44b819c0 100644 --- a/tests/test_transcript_replay_predicates.cpp +++ b/tests/test_transcript_replay_predicates.cpp @@ -81,6 +81,27 @@ TEST_CASE("replay_user_echo_text strips master AGENTS/QUERY preamble") { // QUERY marker without an AGENTS roster is not stripped. m.content = "see\n\nQUERY: docs"; CHECK(replay_user_echo_text(m) == "see\n\nQUERY: docs"); + + // User text after the orchestrator marker may embed the same + // substring. The first QUERY (after AGENTS) is the boundary; + // rfind would echo only the suffix. + m.content = + "AGENTS — delegate with /agent :\n" + " research [research-analyst]\n" + "\n" + "QUERY: compare\n\nQUERY: docs vs the README"; + CHECK(replay_user_echo_text(m) == "compare\n\nQUERY: docs vs the README"); + + m.content = + "[OPEN TODOS] (mark progress as you go):\n" + "1. ship it\n" + "[END OPEN TODOS]\n" + "\n" + "AGENTS: none loaded\n" + "\n" + "QUERY: \n" + "see\n\nQUERY: docs"; + CHECK(replay_user_echo_text(m) == "see\n\nQUERY: docs"); } TEST_CASE("claim_pane_transcript_replay dedupes shared conversation siblings") {