fix(cli): clear stale recording state when its session is gone

If the foreground `bsk record start` process is killed and its session
later disappears, record-session.json keeps pointing at the dead session.
`bsk record stop` then fails with not_found and `bsk record start` refuses
to run until the file is removed by hand.

When record_stop reports not_found, drop the stale state and say so, so
`bsk record stop` is the recovery path its own error message promises.

Refs #342
This commit is contained in:
drakezhang
2026-09-26 12:45:41 +08:00
parent dcb43e5f5f
commit 032b115761
+64 -1
View File
@@ -190,7 +190,8 @@ fn dispatch_stop(args: RecordStopArgs, format: Format) -> Result<(), CliError> {
Method::ToolRecordStop,
Some(params),
TOOL_IPC_TIMEOUT,
)?;
)
.map_err(|err| forget_stale_recording(err, &session_id))?;
let run_result: Result<(), CliError> = (|| {
let exported = export_with_recovery(&args.output, &result.trace)?;
@@ -215,6 +216,29 @@ fn dispatch_stop(args: RecordStopArgs, format: Format) -> Result<(), CliError> {
render_finish(&trace, &args.output, &exported, format)
}
/// A `not_found` stop means the session or its recording is gone, so the
/// saved pointer can never be stopped; drop it so `record start` works again.
fn forget_stale_recording(err: CliError, session_id: &str) -> CliError {
let CliError::Rpc {
code: ErrorCode::NotFound,
message,
data,
source,
} = err
else {
return err;
};
record_state::clear();
CliError::Rpc {
code: ErrorCode::NotFound,
message: format!(
"{message}; cleared the stale recording state for session {session_id}, so `bsk record start` can run again"
),
data,
source,
}
}
/// When `record start` omitted `--url` and the default example.com page
/// never loaded, rewrite the RPC error so the CLI hint points at `--url`.
fn annotate_default_start_page_error(err: CliError, user_url: Option<&str>) -> CliError {
@@ -408,6 +432,45 @@ mod tests {
});
}
#[test]
fn stop_of_a_vanished_session_unblocks_record_start() {
with_temp_home(|| {
record_state::write("megi").unwrap();
let err = CliError::from_rpc(bsk_protocol::RpcError {
code: ErrorCode::NotFound,
message: "session not registered or already stopped".into(),
data: None,
});
let err = forget_stale_recording(err, "megi");
assert_eq!(err.code(), Some(ErrorCode::NotFound));
assert!(
err.to_string()
.contains("cleared the stale recording state")
);
assert!(record_state::read().is_err());
prepare_record_start(Path::new("trace")).unwrap();
});
}
#[test]
fn failed_flush_keeps_the_recording_state_for_a_retry() {
with_temp_home(|| {
record_state::write("oabu").unwrap();
let err = CliError::from_rpc(bsk_protocol::RpcError {
code: ErrorCode::ProtocolError,
message: "failed to flush recorded steps for session oabu".into(),
data: None,
});
let err = forget_stale_recording(err, "oabu");
assert_eq!(err.code(), Some(ErrorCode::ProtocolError));
assert_eq!(record_state::read().unwrap().session_id, "oabu");
});
}
#[test]
fn default_output_is_trace_dir() {
let args = RecordStopArgs {