Show a compaction happening, and what it recovered
The Compacting status had been declared, rendered in four places, and never once emitted: no driver produced it, and the app had no control to ask for a compaction in the first place. Pressing nothing for two minutes and then quietly having less context was the whole experience. The CLI turns out to announce all of it, which was worth measuring rather than guessing at. Driven through /compact against 2.1.237 it emits a `status: "compacting"` line at the start, a `status: null` carrying `compact_result` at the end -- `"failed"` with a sentence saying why, when it does -- and then a `compact_boundary` with the token counts. The same records appear in the CLI's own transcript file with camelCase keys, which is the obvious place to read the shape off and gets every field name wrong. So none of it is inferred here. The driver writes the line and says nothing; the translator reports what the CLI reports. A failed compaction surfaces the CLI's own sentence, which is specific enough to act on. The counts are the part worth keeping afterwards, so they land in the transcript rather than only in a status that vanishes: a session that went from 128,402 tokens to 9,617 has just been given its context back. They are optional throughout, because a compaction whose size nobody reported has to be able to say so -- a zero would read as "recovered nothing". Also here, all found on the way: - `rename_all` renames variants; fields need `rename_all_fields`. Every field in Event was a single word until `pre_tokens`, which went out as snake_case, was not found by the app, and rendered as the "no counts reported" case -- a state it is allowed to be in, so nothing looked wrong. There is now a test on the wire names. - The unparseable-line warning sliced bytes, not chars, on output that is full of em dashes. A panic there kills the task reading the session's stdout, and the session goes deaf with nothing on screen. The other three truncations in the tree already did this correctly. - Echo compacts too, with invented numbers and a real shape, so this screen can be looked at without spending two minutes of somebody's account to reach the state.
This commit is contained in:
1 parent
42131c75d6
commit
5396da76c7
8 files changed
+408
-38
No files matched your search
+53
-24
@@ -1,6 +1,6 @@
|
||||
//! The phase-1 fake driver: no child process, just events. It exists to
|
||||
//! prove the whole pipe -- spawn, transcript, SSE cursors, questions,
|
||||
//! interrupts -- before any AI is involved, and stays useful afterwards as
|
||||
//! interrupts, compaction -- before any AI is involved, and stays useful afterwards as
|
||||
//! a connectivity check that costs no tokens.
|
||||
//!
|
||||
//! Behavior: every message is echoed back as a few streamed text deltas.
|
||||
@@ -36,6 +36,12 @@ use super::driver::{Driver, Event, EventSink, ImageRef, SessionStatus};
|
||||
/// stay fast.
|
||||
const DELTA_DELAY: Duration = Duration::from_millis(50);
|
||||
|
||||
/// How long a fake compaction takes. A real one runs for a minute or two,
|
||||
/// which is too long to sit through when what is being checked is what the
|
||||
/// screen does; this is long enough that the state is visible and short
|
||||
/// enough to wait for.
|
||||
const COMPACT_TIME: Duration = Duration::from_secs(3);
|
||||
|
||||
pub struct EchoDriver {
|
||||
sink: EventSink,
|
||||
/// Whether a turn is in flight, and what arrived during it.
|
||||
@@ -74,6 +80,28 @@ impl EchoDriver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ending a turn is also when anything held during it is taken up -- the
|
||||
/// moment a real CLI would have injected it. One place, because a turn has
|
||||
/// several ways to end (a reply, an interrupt, a compaction) and every one
|
||||
/// of them owes the same answer.
|
||||
fn finish_turn(sink: &EventSink, queued: &Mutex<Vec<String>>, busy: &AtomicBool) {
|
||||
let held = std::mem::take(&mut *queued.lock().unwrap());
|
||||
for text in held {
|
||||
// Announced before it is answered, in that order: a phone showing
|
||||
// the message as pending needs the signal that it has been read,
|
||||
// and the answer is meaningless above a message still drawn as
|
||||
// waiting.
|
||||
let _ = sink.send(Event::MessageTaken { text: text.clone() });
|
||||
let _ = sink.send(Event::AssistantText {
|
||||
delta: format!("\n(taken from the queue) You said: {text}"),
|
||||
});
|
||||
}
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
let _ = sink.send(Event::Status {
|
||||
state: SessionStatus::Idle,
|
||||
});
|
||||
}
|
||||
|
||||
impl Driver for EchoDriver {
|
||||
fn send_user_message(&self, text: String, _images: Vec<ImageRef>) {
|
||||
let sink = self.sink.clone();
|
||||
@@ -141,27 +169,7 @@ impl Driver for EchoDriver {
|
||||
let send = |event: Event| {
|
||||
let _ = sink.send(event);
|
||||
};
|
||||
// Ending a turn is also when anything held during it is taken
|
||||
// up -- the moment a real CLI would have injected it. One
|
||||
// place, because a turn has several ways to end and every one
|
||||
// of them owes the same answer.
|
||||
let finish = || {
|
||||
let held = std::mem::take(&mut *queued.lock().unwrap());
|
||||
for text in held {
|
||||
// Announced before it is answered, in that order: a
|
||||
// phone showing the message as pending needs the
|
||||
// signal that it has been read, and the answer is
|
||||
// meaningless above a message still drawn as waiting.
|
||||
send(Event::MessageTaken { text: text.clone() });
|
||||
send(Event::AssistantText {
|
||||
delta: format!("\n(taken from the queue) You said: {text}"),
|
||||
});
|
||||
}
|
||||
busy.store(false, Ordering::SeqCst);
|
||||
send(Event::Status {
|
||||
state: SessionStatus::Idle,
|
||||
});
|
||||
};
|
||||
let finish = || finish_turn(&sink, &queued, &busy);
|
||||
// Echo takes a message the instant it gets one, but it says so
|
||||
// anyway: a driver that skips this leaves the phone holding a
|
||||
// message it thinks is still queued, and the point of an echo
|
||||
@@ -295,9 +303,30 @@ impl Driver for EchoDriver {
|
||||
});
|
||||
}
|
||||
|
||||
/// A compaction with nothing to compact.
|
||||
///
|
||||
/// The counts are invented, like everything else this driver says --
|
||||
/// what is real is the shape and the order: busy, a pause long enough
|
||||
/// to see, then the result. `Compacting` and `Compacted` are states a
|
||||
/// screen has to draw, and the only other way to reach them is to fill
|
||||
/// a real session's context and spend two minutes of somebody's
|
||||
/// account getting it back.
|
||||
fn compact(&self) {
|
||||
self.emit(Event::Error {
|
||||
message: "echo sessions have nothing to compact".to_string(),
|
||||
let sink = self.sink.clone();
|
||||
let queued = Arc::clone(&self.queued);
|
||||
let busy = Arc::clone(&self.busy);
|
||||
busy.store(true, Ordering::SeqCst);
|
||||
tokio::spawn(async move {
|
||||
let _ = sink.send(Event::Status {
|
||||
state: SessionStatus::Compacting,
|
||||
});
|
||||
tokio::time::sleep(COMPACT_TIME).await;
|
||||
let _ = sink.send(Event::Compacted {
|
||||
pre_tokens: Some(128_402),
|
||||
post_tokens: Some(9_617),
|
||||
trigger: Some("manual".to_string()),
|
||||
});
|
||||
finish_turn(&sink, &queued, &busy);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user