Say when a session is working, and what it was told
Three things a phone could not see, all of them the same shape: the session was doing something and nothing on screen said so. A turn nobody here started never reported itself. `Running` was sent where a message was *sent*, so a session picked up mid-turn, one compacting on its own, or one another agent wrote to sat there reading as idle until it finished. The driver now says it from what it observes -- output that could only come from a turn in flight -- which is the same set of events that already announced a steer, with the ends swapped. An imported session had it worse: nothing but replayed lines ever reaches it, and a status was not among them, so it was permanently whatever it was when it was adopted. Its file does not record a turn ending, but it does record why each assistant message stopped, and `tool_use` versus anything else answers it. A record that says nothing leaves the status alone rather than voting for idle. Messages from other agents were dropped outright: the CLI marks them meta, and this replayed everything except meta. They are now a row of their own, closed by default like a tool call, named for the session that sent it -- not the reader's own bubble, because they did not say it, and a session working on something this phone never asked for is exactly what one of these explains. Measured against a real session file rather than guessed: the peer record carries the sender's name and the message body in `origin`, beside a copy wrapped for the model to read.
This commit is contained in:
1 parent
f18639e4b1
commit
404066fa7d
11 files changed
+577
-29
No files matched your search
@@ -732,6 +732,26 @@ fn translate_line(
|
||||
}
|
||||
}
|
||||
}
|
||||
// A turn nobody here started -- see `proves_a_turn`. Said before
|
||||
// the event that proves it, for the same reason a steer is: the
|
||||
// session was already working when it produced this.
|
||||
let started = {
|
||||
let mut queue = queue.lock().unwrap();
|
||||
let started = proves_a_turn(&event) && !queue.running && !queue.closed;
|
||||
if started {
|
||||
queue.running = true;
|
||||
}
|
||||
started
|
||||
};
|
||||
if started
|
||||
&& sink
|
||||
.send(Event::Status {
|
||||
state: SessionStatus::Running,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if matches!(
|
||||
event,
|
||||
Event::Status {
|
||||
@@ -747,6 +767,39 @@ fn translate_line(
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether this event could only have come from a turn in flight.
|
||||
///
|
||||
/// The turn this side starts is announced where it is started, and that
|
||||
/// covers the common case and nothing else. Everything below happens
|
||||
/// without a phone asking for it: a compaction the CLI decided on by
|
||||
/// itself, a session adopted while it was already mid-turn, a message
|
||||
/// that reached the conversation by some route other than this server --
|
||||
/// another agent writing to it, or somebody at the terminal. In all of
|
||||
/// them the CLI is plainly working and the only thing that would ever
|
||||
/// have said so is a `Running` nobody sent, so the session sits there
|
||||
/// reading as idle until the turn ends.
|
||||
///
|
||||
/// So the driver says it from what it observes rather than from what it
|
||||
/// was asked to do, and this is the same set as [`announces_a_steer`]
|
||||
/// with the ends swapped: that one takes the `Idle` that closes a turn
|
||||
/// and this one takes the states that open one. `Idle` is the pair to
|
||||
/// this -- it is where `running` goes back to false, a few lines above
|
||||
/// where it is set here.
|
||||
fn proves_a_turn(event: &Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
Event::AssistantText { .. }
|
||||
| Event::ToolStart { .. }
|
||||
| Event::ToolUpdate { .. }
|
||||
| Event::ToolEnd { .. }
|
||||
| Event::Question { .. }
|
||||
| Event::Compacted { .. }
|
||||
| Event::Status {
|
||||
state: SessionStatus::Compacting | SessionStatus::AwaitingInput
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this event proves the CLI has consumed anything written to it
|
||||
/// since the last one did.
|
||||
///
|
||||
@@ -985,6 +1038,73 @@ mod tests {
|
||||
assert!(queue.closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_turn_this_side_did_not_start_still_reports_as_running() {
|
||||
// The case: a session picked up while it was already working, or
|
||||
// one another agent wrote to. Nothing called `send_user_message`,
|
||||
// so the only thing that can say the session is busy is what it
|
||||
// is observed doing.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let (sink, mut received) = mpsc::unbounded_channel();
|
||||
let state = Arc::new(Mutex::new(Translator::new(dir.path().to_path_buf())));
|
||||
let queue = Arc::new(Mutex::new(Queue::default()));
|
||||
|
||||
let text = r#"{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"working"}},"parent_tool_use_id":null}"#;
|
||||
assert!(translate_line(text, dir.path(), &state, &sink, &queue));
|
||||
assert_eq!(
|
||||
received.try_recv().ok(),
|
||||
Some(Event::Status {
|
||||
state: SessionStatus::Running
|
||||
}),
|
||||
"a turn in flight has to be reported before the output proving it"
|
||||
);
|
||||
assert!(matches!(
|
||||
received.try_recv().ok(),
|
||||
Some(Event::AssistantText { .. })
|
||||
));
|
||||
|
||||
// Once only: the turn is known to be running now, and a status per
|
||||
// delta would be a status per word.
|
||||
assert!(translate_line(text, dir.path(), &state, &sink, &queue));
|
||||
assert!(matches!(
|
||||
received.try_recv().ok(),
|
||||
Some(Event::AssistantText { .. })
|
||||
));
|
||||
|
||||
// And the end of the turn puts it back, so the next one is
|
||||
// reported the same way.
|
||||
let done = r#"{"type":"result","subtype":"success","is_error":false,"usage":{}}"#;
|
||||
assert!(translate_line(done, dir.path(), &state, &sink, &queue));
|
||||
assert_eq!(
|
||||
received.try_recv().ok(),
|
||||
Some(Event::Status {
|
||||
state: SessionStatus::Idle
|
||||
})
|
||||
);
|
||||
assert!(!queue.lock().unwrap().running);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_from_a_process_that_has_gone_does_not_revive_the_turn() {
|
||||
// `close` is what says the process is gone and reports the
|
||||
// messages that died with it. Anything still in the pipe after
|
||||
// that must not put the session back to work, because there is
|
||||
// nothing left to do the work.
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let (sink, mut received) = mpsc::unbounded_channel();
|
||||
let state = Arc::new(Mutex::new(Translator::new(dir.path().to_path_buf())));
|
||||
let queue = Arc::new(Mutex::new(Queue::default()));
|
||||
queue.lock().unwrap().close(&sink, "the session ended");
|
||||
|
||||
let text = r#"{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"late"}},"parent_tool_use_id":null}"#;
|
||||
assert!(translate_line(text, dir.path(), &state, &sink, &queue));
|
||||
assert!(matches!(
|
||||
received.try_recv().ok(),
|
||||
Some(Event::AssistantText { .. })
|
||||
));
|
||||
assert!(!queue.lock().unwrap().running);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_an_empty_queue_says_nothing() {
|
||||
let (sink, mut received) = mpsc::unbounded_channel();
|
||||
|
||||
Reference in new issue
Block a user