Review M2b: accept with one follow-up task; record lessons

All ten tasks pass the checklist, the gate, the audit and the device
checks, including a four-turn conversation with a loopd restart and no
cache loss. Reading and probing found four low defects: the busy guard
is released before the final frame on the main path but not on the
three error paths, its Drop skips a poisoned lock, an unreadable
core.md is treated as missing, and bxctl's interactive loop exits on a
failed turn. Task 11 carries the fixes with two new tests, checked
against a fixed copy of the branch.

The Model column is filled in (all Ornith) and one malformed row is
repaired. Two rules are promoted to AGENTS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 20:47:03 -07:00
co-authored by Claude Fable 5.1
parent 34951084cc
commit 2be8581a0c
8 changed files with 218 additions and 18 deletions
@@ -57,6 +57,8 @@ fn fake_loopd(events: Vec<TurnEvent>, end: End) -> FakeLoopd {
let id = request.id;
let end = if turn.resume && turn.content == "trigger-no-such-session" {
End::Error(ErrorCode::NoSuchSession, "session x does not exist")
} else if turn.content == "trigger-turn-limit" {
End::Error(ErrorCode::TurnLimit, "the turn hit a limit")
} else {
end.clone()
};
@@ -417,3 +419,45 @@ fn bad_arguments_print_usage() {
.unwrap();
assert_eq!(output.status.code(), Some(2));
}
/// A failed turn is reported, and the conversation goes on: the session still exists and the
/// next line is a new turn on it.
#[test]
fn interactive_mode_survives_a_failed_turn() {
let fake = fake_loopd(
vec![TurnEvent::Content {
text: "ok".to_string(),
}],
End::Done(TurnDone {
content: "ok".to_string(),
usage: usage(),
}),
);
let mut child = Command::new(env!("CARGO_BIN_EXE_bxctl"))
.args(["chat", "--socket"])
.arg(&fake.socket)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
{
let mut stdin = child.stdin.take().unwrap();
std::io::Write::write_all(&mut stdin, b"first\ntrigger-turn-limit\nthird\n/quit\n")
.unwrap();
}
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"a failed turn does not end the chat"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("turn limit: the turn hit a limit"),
"{stderr}"
);
let turns = fake.turns.lock().unwrap();
assert_eq!(turns.len(), 3, "the turn after the failure was sent");
assert!(turns[2].resume, "and it resumed the same session");
assert_eq!(turns[2].session, turns[0].session);
}