{"record":{"id":"ccd16249fff6c478","repo":"Hmbown/CodeWhale","slug":"compaction-coverage-floor-a-last-round-user-message-was","errorCode":null,"errorMessage":"Compaction coverage floor: a last-round user message was dropped; history was not replaced.","messagePattern":"Compaction coverage floor: a last-round user message was dropped; history was not replaced\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/compaction/last_round.rs","lineNumber":371,"sourceCode":"    })\n}\n\npub(crate) fn validate_last_round_coverage(\n    original: &[Message],\n    replacement: &[Message],\n) -> Result<()> {\n    let last_round = protected_last_round(original);\n    if last_round.is_empty() {\n        return Ok(());\n    }\n    // Every user turn in the round, not the first one `find_map` happens to\n    // reach. `last_round_start` walks back past a toolless tail to the previous\n    // tool-bearing turn, so the round routinely spans two user messages -- and\n    // checking only the earliest let a rewrite drop the *latest* one, which is\n    // the turn this whole contract exists to keep.\n    for text in last_round.iter().copied().filter_map(user_prompt_text_of) {\n        if !survives(&text, replacement, user_prompt_text_of) {\n            anyhow::bail!(\n                \"Compaction coverage floor: a last-round user message was dropped; history was not replaced.\"\n            );\n        }\n    }\n    for id in last_round.iter().copied().flat_map(tool_result_ids) {\n        if !replacement\n            .iter()\n            .any(|message| has_tool_result_id(message, &id))\n        {\n            anyhow::bail!(\n                \"Compaction coverage floor: last-round tool result {id} was dropped; history was not replaced.\"\n            );\n        }\n    }\n    // The call, not just its result. Keeping a tool_result whose tool_use was\n    // summarized away leaves an orphaned result that providers reject outright.\n    for id in last_round.iter().copied().flat_map(tool_use_ids) {\n        if !replacement","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/compaction/last_round.rs#L353-L389","documentation":"The last-round survival contract validates that a compaction replacement history keeps every message of the most recent conversation round. This fires when any user prompt text from the last round is absent from the replacement, so the rewrite is rejected and history is not replaced. It exists because the round can span two user messages and dropping the latest one silently loses the user's most recent request.","triggerScenarios":"`validate_last_round_coverage` (via `validate_survival_contract` or coverage-floor tests) finds a last-round user message whose text does not appear in the replacement history.","commonSituations":"A custom or newly-introduced compaction rewrite drops the latest user turn; an off-by-one in `last_round_start` boundary computation; editing compaction code so older turns are summarized but the current user turn is also swallowed.","solutions":["Fix the compaction rewrite so every last-round user message is carried into the replacement history verbatim.","Re-check the `last_round_start` computation — it must walk past a toolless tail to the previous tool-bearing turn and include ALL user messages in the round, not just the earliest.","Run the coverage-floor tests (`coverage_floor_rejects_*`) to see which scenario reproduces.","Ensure summaries are prepended/summarized only for turns strictly before the last round."],"exampleFix":"// before: replacement only summarizes, dropping the latest user turn\nlet replacement = vec![summary_message];\n// after: keep the whole last round\nlet replacement = [summary_message, last_round.as_slice()].concat();","handlingStrategy":"validation","validationCode":"fn keeps_last_round_users(last_round: &[Message], replacement: &[Message]) -> bool {\n    last_round.iter().filter_map(user_prompt_text_of)\n        .all(|t| replacement.iter().any(|m| user_prompt_text_of(m) == Some(t)))\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = compaction::apply_replacement(&replacement) {\n    if e.to_string().contains(\"coverage floor\") { /* keep original history */ }\n}","preventionTips":["Only summarize turns strictly before the last round","Include every user message of the last round (the round can span two)","Add coverage tests when changing compaction code","Never let a summary displace the current user turn"],"tags":["compaction","coverage-floor","last-round"],"backgroundTag":"internal-invariant-violation","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}