diff --git a/CHANGELOG.md b/CHANGELOG.md index c680a0dc..34c6d65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ 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. - **Bearer scheme matches RFC 6750 / RFC 9110.** `extract_bearer` now treats the auth-scheme as case-insensitive and skips `1*SP` after it, so `bearer` / `BEARER` and extra spaces no longer 401 a valid token. 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") {