Let a session drop its context without ending

Adds `Event::Cleared`, `SessionCommand::Clear`, and `Driver::clear`, so
`POST /sessions/{id}/command {"text": "/clear"}` does for a session what
the CLI's own `/clear` does for a terminal.

The marker is a divider, not a truncation: everything above it stays in
the transcript, because that is the only copy of the conversation the
phone has and a person scrolling back is a different question from what
the model is given. It also makes clearing mean one thing across
drivers -- `claude` sends `/clear` and the CLI answers with a fresh
`init` whose new session_id the reader already persists as the resume
token, so the next launch resumes the cleared conversation with nothing
to keep in step; `llama` needs no state at all, since `conversation()`
already folds the transcript and now folds from the last marker; `echo`
emits the marker alone, so the phone's divider and scroll behaviour can
be exercised without spending a real session's context.

That fold is why `Cleared` is documented as load-bearing rather than
decorative. For any driver that rebuilds its conversation from the
transcript, this marker decides what the model sees, and treating it as
something only the phone draws would silently put the cleared
conversation back in front of the model at full price.

Clear rides the existing boundary pump like any other SessionCommand, so
one arriving mid-turn waits exactly as a compaction does, and nothing
grows a second way to wait.

Removes `--autocompact` in the same change, because clearing is the
cheaper answer to the problem it was added for and Bryan would rather
manage context that way. Keeping the measurement here, since it was the
reason for the constant and is worth more than the constant was:
context returned to 70-85k within ten calls of a compaction; a
compaction took 104,346 to 147,671 ms; compaction cost that session
2,655,508 tokens across six boundaries, of which the single automatic
one at the 1M ceiling was 1,696,870. Clearing costs nothing, because
nothing is sent.

70 tests, clippy clean, rustfmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-29 20:06:04 -04:00
1 parent 797513bb86
commit 1e3eec8cf0
5 files changed
+122 -34

No files matched your search

+41
View File
@@ -244,6 +244,30 @@ pub enum Event {
id: String,
text: String,
},
/// The conversation was cleared: everything above this is still in
/// the record but is no longer in the session's context.
///
/// Nothing is deleted. A transcript is the thing a person scrolls
/// back through, and a session that dropped its history from the
/// screen as well as from the model would lose the only copy the
/// phone has -- so this is a divider, not a truncation, and the
/// events before it stay exactly where they were.
///
/// It is also what makes clearing mean the same thing for every
/// driver, which is why the marker lives here rather than in one
/// dialect: `llama` folds its conversation out of the transcript and
/// simply folds from the last one of these, and `claude` starts a new
/// CLI conversation behind it.
///
/// **Load-bearing, not decorative.** For any driver that rebuilds its
/// conversation from the transcript, this marker decides what the
/// model is given -- dropping it, or treating it as something only
/// the phone draws, silently puts a cleared conversation back in
/// front of the model at full cost. Today `llama::conversation` is
/// the only fold that reads it, which is the reason to write this
/// down rather than leave it to be inferred from a second example
/// that does not exist yet.
Cleared,
Error {
message: String,
},
@@ -260,6 +284,7 @@ pub enum Event {
#[derive(Debug, Clone, PartialEq)]
pub enum SessionCommand {
Compact,
Clear,
SetTitle(String),
Raw(String),
}
@@ -270,6 +295,7 @@ impl SessionCommand {
pub fn label(&self) -> String {
match self {
Self::Compact => "/compact".to_string(),
Self::Clear => "/clear".to_string(),
Self::SetTitle(title) => format!("/rename {title}"),
Self::Raw(text) => text.clone(),
}
@@ -279,6 +305,7 @@ impl SessionCommand {
pub fn apply(&self, driver: &dyn Driver) {
match self {
Self::Compact => driver.compact(),
Self::Clear => driver.clear(),
Self::SetTitle(title) => driver.set_title(title),
Self::Raw(text) => driver.run_command(text),
}
@@ -371,6 +398,20 @@ pub trait Driver: Send + Sync {
fn run_command(&self, text: &str);
/// pi: native compaction; claude: `/compact`.
fn compact(&self);
/// Drops the conversation so far without ending the session.
///
/// The cheap half of managing a long session, and the reason it is a
/// driver operation rather than a manager one: compaction *reads* the
/// whole conversation in order to summarise it, so on a large context
/// it is itself one of the most expensive requests the session will
/// make -- measured at 1.7 million tokens for a single automatic
/// compaction on 2026-08-29. Clearing costs nothing, because nothing
/// is sent.
///
/// Every implementation emits [`Event::Cleared`] so the transcript
/// carries the divider whatever the dialect did behind it.
fn clear(&self);
/// Stop attending to the process but leave it running, because this
/// server is going away and means to adopt it again when it comes
/// back.