Start a stopped session's process for a command too
Same reasoning as the message path a commit ago, and the same objection
to leaving it out: a command is something somebody asked the session to
do, and answering "its process has exited" hands back the work of
starting one. `/compact` on a stopped session is the case that shows it
-- what is being asked for is exactly what a stopped session needs
before it is useful again.
`POST /sessions/{id}/command` and `/compact` now go through
`SessionManager::run_command`. A rename is deliberately not one of them:
it is persisted and listed whether or not a process ever hears about it,
so starting a CLI to tell it a name would be spending a resume on
nothing. It stays a forward to a process that happens to be there.
A command needs one thing a message did not. `Commands::submit` refuses
on `Exited`, and a driver that has just started a process announces
`Idle` through the sink rather than writing it -- so a command judged
against the session's own status would be refused by the word the start
had just replaced, in a window narrow enough that only a test reliably
hits it. `start_if_exited` returning `Exited` is what says a process was
started, so the status the command is judged against comes from there
rather than from a re-read the pump may not have caught up with. The
test fails without it.
`LiveSession::compact` went with this: `/compact` the route and
"/compact" the typed command were two ways to the same command, and now
there is one.
Verified over the API against a stand-in CLI: with the session reporting
`exited`, both `/clear` and `POST /compact` started the process and were
delivered -- transcript order `idle`, `commandSent`, `running`, `idle`,
with no "this session's process has exited" anywhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a122f7b25
commit
1a132b4de3
4 files changed
+115
-36
No files matched your search
+68
-14
@@ -399,13 +399,6 @@ impl LiveSession {
|
||||
self.driver().detach();
|
||||
}
|
||||
|
||||
/// Compacts at the next boundary. Through the command queue like
|
||||
/// every other instruction to the session, so pressing it during a
|
||||
/// turn holds rather than writing into that turn.
|
||||
pub fn compact(&self) {
|
||||
self.run_command(SessionCommand::Compact);
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<SeqEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
@@ -1150,6 +1143,42 @@ impl SessionManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs one of the session's own commands, starting its process first
|
||||
/// if that session has none.
|
||||
///
|
||||
/// The same reasoning as [`SessionManager::send_message`], and for the
|
||||
/// same reason it is not left to each caller: a command is something
|
||||
/// somebody asked the session to do, and answering "its process has
|
||||
/// exited" hands back the work of starting one. `/compact` on a
|
||||
/// stopped session is the case that shows it -- the thing being asked
|
||||
/// for is exactly what a stopped session needs before it is useful
|
||||
/// again.
|
||||
///
|
||||
/// Renaming is deliberately not here. It is persisted and listed
|
||||
/// whether or not a process ever hears about it, so starting a CLI to
|
||||
/// tell it a name would be spending a resume on nothing -- see
|
||||
/// [`SessionManager::rename_session`], which forwards it to a process
|
||||
/// that happens to be there.
|
||||
pub fn run_command(&self, id: &str, command: SessionCommand) -> Result<()> {
|
||||
// Judged against the status *after* the start, not the one that
|
||||
// caused it. A driver that has just started a process announces
|
||||
// `idle` through the sink and the pump may not have recorded it
|
||||
// yet, so reading the session's own status here would refuse the
|
||||
// command the start was for -- `Commands::submit` refuses on
|
||||
// `Exited`, which is exactly the word that has just stopped being
|
||||
// true. `start_if_exited` returning `Exited` is what says a process
|
||||
// was started; anything else is a status nothing has invalidated.
|
||||
let status = match self.start_if_exited(id)? {
|
||||
SessionStatus::Exited => SessionStatus::Idle,
|
||||
found => found,
|
||||
};
|
||||
self.session(id)
|
||||
.with_context(|| format!("no session {id}"))?
|
||||
.commands
|
||||
.submit(command, status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts a process for the session if it is known to have exited, and
|
||||
/// reports what the session was found to be doing either way. `Exited`
|
||||
/// is therefore the one returned value that means something was started.
|
||||
@@ -2612,16 +2641,22 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
/// Sending is an instruction that means "now", so it does not answer
|
||||
/// that the session's process has gone -- it starts one and delivers
|
||||
/// the message to it.
|
||||
/// A message and a command both mean "now", so neither answers that the
|
||||
/// session's process has gone -- they start one and go to it.
|
||||
///
|
||||
/// Refusing was the old behaviour and it was work handed back: read the
|
||||
/// status word, find the other button, press it, type the message
|
||||
/// again. `--resume` puts the new process on the same conversation, so
|
||||
/// the message it reads is the one that was typed.
|
||||
/// status word, find the other button, press it, type the thing again.
|
||||
/// `--resume` puts the new process on the same conversation, so what it
|
||||
/// reads is what was typed.
|
||||
///
|
||||
/// Both halves in one test because they are one rule. A command is the
|
||||
/// half that can fail on its own: `Commands::submit` refuses on
|
||||
/// `Exited`, and the start it has just been given announces `Idle`
|
||||
/// through the sink rather than writing it -- so a command judged
|
||||
/// against the session's own status would be refused by the word the
|
||||
/// start replaced, in a window a test is the only thing likely to hit.
|
||||
#[tokio::test]
|
||||
async fn a_message_starts_the_process_a_stopped_session_has_not_got() {
|
||||
async fn an_instruction_starts_the_process_a_stopped_session_has_not_got() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let config_path = dir.path().join("config.ron");
|
||||
let data_dir = dir.path().join("sessions");
|
||||
@@ -2657,6 +2692,25 @@ mod tests {
|
||||
// delivered to a session still reporting `exited` is one the phone
|
||||
// draws under a Start button.
|
||||
assert_ne!(manager.sessions()[0].status, SessionStatus::Exited);
|
||||
|
||||
let _ = session.sink.send(Event::Status {
|
||||
state: SessionStatus::Exited,
|
||||
});
|
||||
collect_until(&mut rx, |event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Status {
|
||||
state: SessionStatus::Exited
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
manager
|
||||
.run_command(&info.id, SessionCommand::Clear)
|
||||
.expect("clear a stopped session");
|
||||
collect_until(&mut rx, |event| matches!(event, Event::Cleared)).await;
|
||||
assert_ne!(manager.sessions()[0].status, SessionStatus::Exited);
|
||||
}
|
||||
|
||||
/// The status is a claim about a process, and the process record is
|
||||
|
||||
Reference in new issue
Block a user