Skip to content

Commit 74aa1c9

Browse files
author
CodeWhale Bot
committed
fix(compaction): keep last tool round and unstick CI (#4394)
Drop the unused validate_last_round_coverage re-export that failed Lint. Walk last_round_start back to the last tool-bearing user round instead of one hop, so two trailing toolless turns cannot drop a tool result behind the summary. Sync the TUI changelog slice with root. Add a language-invariant fixture matrix and a Node coverage validator next to the survival contract. There is no Go runtime in this repository yet. Evidence: cargo check -p codewhale-tui --locked cargo clippy -p codewhale-tui --lib --locked -- -A clippy::all -D unused-imports cargo test -p codewhale-tui --lib --offline --locked -- compaction context_inspector last_round -> 128 passed; 0 failed node crates/tui/src/compaction/validate_survival_contract.mjs -> ok 9 survival-contract fixtures Refs: #4394 Signed-off-by: CodeWhale Bot <bot@codewhale.net>
1 parent a984d72 commit 74aa1c9

7 files changed

Lines changed: 473 additions & 33 deletions

File tree

crates/tui/CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- Compaction replacement history keeps a bounded last user round (assistant +
1616
tool results) instead of dropping them behind a summary. `/context` names
17-
the compaction path and `/anchor` survival (#4394).
17+
the compaction path and `/anchor` survival. Failed compact still does not
18+
replace live history (#4394).
1819

1920
- TUI: startup no longer presents an approximate ASCII or block-glyph whale as
2021
the product mark. It keeps the direct Tideline prompt while exact-raster
@@ -27,8 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2728
### Added
2829

2930
- Compaction publishes a structured survival contract for session-tree
30-
journal entry types (`src/compaction/SURVIVAL_CONTRACT.md`) and fails
31-
closed when the last user round, tool results, `/anchor` text, or
31+
journal entry types (`crates/tui/src/compaction/SURVIVAL_CONTRACT.md`) and
32+
fails closed when the last user round, tool results, `/anchor` text, or
3233
checkpoint receipt would vanish (#4394).
3334

3435
- Website: the public site moves to the Tideline deep-ocean design language

crates/tui/src/compaction.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ mod last_round;
1818
#[cfg(test)]
1919
#[path = "compaction/survival_contract.rs"]
2020
mod survival_contract;
21+
pub(crate) use last_round::last_round_start;
2122
pub use last_round::{
2223
CompactionCoverage, CompactionKeep, CompactionPath, LastCompactionSnapshot,
2324
inspect_compaction_keep, last_round_kept_count, pinned_anchors_text,
2425
};
25-
pub(crate) use last_round::{last_round_start, validate_last_round_coverage};
2626

2727
/// Configuration for conversation compaction behavior.
2828
///
@@ -2037,7 +2037,8 @@ mod tests {
20372037
user_text_of(retained.last().unwrap()).as_deref(),
20382038
Some(text.as_str())
20392039
);
2040-
validate_last_round_coverage(&messages, &retained[..retained.len() - 1]).unwrap();
2040+
last_round::validate_last_round_coverage(&messages, &retained[..retained.len() - 1])
2041+
.unwrap();
20412042
}
20422043

20432044
#[test]
@@ -2047,7 +2048,7 @@ mod tests {
20472048
msg("assistant", "session_store::roundtrip panics on reload."),
20482049
];
20492050
let gutting = vec![msg("user", "What failed?")];
2050-
let error = validate_last_round_coverage(&original, &gutting)
2051+
let error = last_round::validate_last_round_coverage(&original, &gutting)
20512052
.expect_err("dropping last-round assistant text must fail closed");
20522053
assert!(error.to_string().contains("assistant"), "{error}");
20532054
}

crates/tui/src/compaction/SURVIVAL_CONTRACT.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ work.
6767
Rust now:
6868

6969
- `last_round::build_replacement_history` keeps the bounded last user round
70-
(assistant + tool results) and appends one checkpoint receipt.
70+
(assistant + tool results) and appends one checkpoint receipt. A trailing
71+
toolless user/assistant tail still walks back to the last tool-bearing
72+
round; chat-only sessions keep only the latest user turn.
7173
- `validate_last_round_coverage` refuses the rewrite if that round's user
7274
text, tool-result ids, or assistant output would vanish.
7375
- `validate_survival_contract` also refuses a missing checkpoint receipt,
@@ -76,4 +78,11 @@ Rust now:
7678
- `compact_messages_safe` never mutates the caller's live history; the host
7779
commits only after `Ok`.
7880

81+
The language-invariant fixture matrix is
82+
`crates/tui/src/compaction/fixtures/matrix.json`. Rust loads it in
83+
`last_round` tests. `validate_survival_contract.mjs` is the same coverage
84+
floor for a later TypeScript strategy (`node validate_survival_contract.mjs`).
85+
There is no Go runtime in this repository yet; a Go validator remains a
86+
follow-up when a Go strategy exists.
87+
7988
`/context` names the compaction path and `/anchor` survival.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
{
2+
"schema_version": 1,
3+
"cases": [
4+
{
5+
"id": "last-round-tools-kept",
6+
"expect": "pass",
7+
"last_round_start": 0,
8+
"original": [
9+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
10+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
11+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
12+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]}
13+
],
14+
"replacement": [
15+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
16+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
17+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
18+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]},
19+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: suite still failing"}]}
20+
]
21+
},
22+
{
23+
"id": "last-round-tools-dropped",
24+
"expect": "fail",
25+
"last_round_start": 0,
26+
"original": [
27+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
28+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
29+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
30+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]}
31+
],
32+
"replacement": [
33+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
34+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: and kept going"}]}
35+
]
36+
},
37+
{
38+
"id": "last-round-assistant-dropped",
39+
"expect": "fail",
40+
"last_round_start": 0,
41+
"original": [
42+
{"role": "user", "content": [{"type": "text", "text": "What failed?"}]},
43+
{"role": "assistant", "content": [{"type": "text", "text": "session_store::roundtrip panics on reload."}]}
44+
],
45+
"replacement": [
46+
{"role": "user", "content": [{"type": "text", "text": "What failed?"}]},
47+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: missing assistant"}]}
48+
]
49+
},
50+
{
51+
"id": "toolless-tail-keeps-prior-tools",
52+
"expect": "pass",
53+
"last_round_start": 0,
54+
"original": [
55+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
56+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
57+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
58+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]},
59+
{"role": "user", "content": [{"type": "text", "text": "ok thanks"}]},
60+
{"role": "assistant", "content": [{"type": "text", "text": "you're welcome"}]},
61+
{"role": "user", "content": [{"type": "text", "text": "one more thing"}]},
62+
{"role": "assistant", "content": [{"type": "text", "text": "sure"}]}
63+
],
64+
"replacement": [
65+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
66+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
67+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
68+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]},
69+
{"role": "user", "content": [{"type": "text", "text": "ok thanks"}]},
70+
{"role": "assistant", "content": [{"type": "text", "text": "you're welcome"}]},
71+
{"role": "user", "content": [{"type": "text", "text": "one more thing"}]},
72+
{"role": "assistant", "content": [{"type": "text", "text": "sure"}]},
73+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: keep the failing test result"}]}
74+
]
75+
},
76+
{
77+
"id": "toolless-tail-drops-prior-tools",
78+
"expect": "fail",
79+
"last_round_start": 0,
80+
"original": [
81+
{"role": "user", "content": [{"type": "text", "text": "Run the failing test."}]},
82+
{"role": "assistant", "content": [{"type": "text", "text": "Running."}]},
83+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
84+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "test session_store::roundtrip ... FAILED"}]},
85+
{"role": "user", "content": [{"type": "text", "text": "ok thanks"}]},
86+
{"role": "assistant", "content": [{"type": "text", "text": "you're welcome"}]},
87+
{"role": "user", "content": [{"type": "text", "text": "one more thing"}]},
88+
{"role": "assistant", "content": [{"type": "text", "text": "sure"}]}
89+
],
90+
"replacement": [
91+
{"role": "user", "content": [{"type": "text", "text": "one more thing"}]},
92+
{"role": "assistant", "content": [{"type": "text", "text": "sure"}]},
93+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: dropped the tool round"}]}
94+
]
95+
},
96+
{
97+
"id": "chat-only-last-round",
98+
"expect": "pass",
99+
"last_round_start": 2,
100+
"original": [
101+
{"role": "user", "content": [{"type": "text", "text": "hello"}]},
102+
{"role": "assistant", "content": [{"type": "text", "text": "hi"}]},
103+
{"role": "user", "content": [{"type": "text", "text": "how are you"}]},
104+
{"role": "assistant", "content": [{"type": "text", "text": "fine"}]}
105+
],
106+
"replacement": [
107+
{"role": "user", "content": [{"type": "text", "text": "how are you"}]},
108+
{"role": "assistant", "content": [{"type": "text", "text": "fine"}]},
109+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: continued the chat"}]}
110+
]
111+
},
112+
{
113+
"id": "dropped-anchor",
114+
"expect": "fail",
115+
"anchors": "ship 0.9.12",
116+
"last_round_start": 0,
117+
"original": [
118+
{"role": "user", "content": [{"type": "text", "text": "Keep the pin."}]},
119+
{"role": "assistant", "content": [{"type": "text", "text": "Anchored."}]}
120+
],
121+
"replacement": [
122+
{"role": "user", "content": [{"type": "text", "text": "Keep the pin."}]},
123+
{"role": "assistant", "content": [{"type": "text", "text": "Anchored."}]},
124+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: progress without the pin"}]}
125+
]
126+
},
127+
{
128+
"id": "missing-receipt",
129+
"expect": "fail",
130+
"anchors": "ship 0.9.12",
131+
"last_round_start": 0,
132+
"original": [
133+
{"role": "user", "content": [{"type": "text", "text": "Keep the pin."}]},
134+
{"role": "assistant", "content": [{"type": "text", "text": "Anchored."}]}
135+
],
136+
"replacement": [
137+
{"role": "user", "content": [{"type": "text", "text": "Keep the pin."}]},
138+
{"role": "assistant", "content": [{"type": "text", "text": "Anchored."}]}
139+
]
140+
},
141+
{
142+
"id": "duplicate-checkpoint",
143+
"expect": "fail",
144+
"last_round_start": 1,
145+
"original": [
146+
{"role": "user", "content": [{"type": "text", "text": "older"}]},
147+
{"role": "user", "content": [{"type": "text", "text": "Run the suite now."}]},
148+
{"role": "assistant", "content": [{"type": "text", "text": "Rerunning."}]},
149+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
150+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "ok"}]},
151+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: first handoff"}]}
152+
],
153+
"replacement": [
154+
{"role": "user", "content": [{"type": "text", "text": "Run the suite now."}]},
155+
{"role": "assistant", "content": [{"type": "text", "text": "Rerunning."}]},
156+
{"role": "assistant", "content": [{"type": "tool_use", "id": "live", "name": "Bash", "input": {"command": "cargo test"}}]},
157+
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "live", "content": "ok"}]},
158+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: first handoff"}]},
159+
{"role": "user", "content": [{"type": "text", "text": "Another language model started to solve this problem: second handoff"}]}
160+
]
161+
}
162+
]
163+
}

0 commit comments

Comments
 (0)