operator control: /compact slash command + endpoint

new POST /api/compact on the per-agent web UI: spawns
turn::compact_session in the background so the http handler returns
immediately. claude runs '/compact' over the persistent --continue
session; output streams into the live panel like any other turn.

slash command /compact wired to the new endpoint. SLASH_COMMANDS list
now lists all four (/help /clear /cancel /compact). postCancelTurn +
postCompact share a postSimple() helper.

deliberately not gated against an in-flight turn — claude's own
session lock will reject a concurrent compact and the failure
surfaces as a Note in the live panel.
This commit is contained in:
müde 2026-05-15 19:56:53 +02:00
parent 5ee65d2f15
commit c9647f4106
2 changed files with 45 additions and 7 deletions

View file

@ -80,6 +80,7 @@ pub async fn serve(
.route("/login/code", post(post_login_code))
.route("/login/cancel", post(post_login_cancel))
.route("/api/cancel", post(post_cancel_turn))
.route("/api/compact", post(post_compact))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = tokio::net::TcpListener::bind(addr)
@ -274,6 +275,37 @@ async fn post_login_cancel(State(state): State<AppState>) -> Response {
Redirect::to("/").into_response()
}
/// Operator-initiated session compaction. Spawns `turn::compact_session`
/// in the background — the HTTP handler returns immediately so the
/// async-form spinner can clear. Output (claude's compaction stream,
/// the "/compact done" note) lands in the live event panel like any
/// other turn. If a regular turn is in flight, claude's own session
/// lock will reject this one and we surface the error as a Note.
async fn post_compact(State(state): State<AppState>) -> Response {
let bus = state.bus.clone();
let socket = state.socket.clone();
tokio::spawn(async move {
bus.emit(crate::events::LiveEvent::Note(
"operator: /compact — running on persistent session".into(),
));
let settings = match crate::turn::write_settings(&socket).await {
Ok(p) => p,
Err(e) => {
bus.emit(crate::events::LiveEvent::Note(format!(
"/compact failed: settings write — {e:#}"
)));
return;
}
};
if let Err(e) = crate::turn::compact_session(&settings, &bus).await {
bus.emit(crate::events::LiveEvent::Note(format!(
"/compact failed: {e:#}"
)));
}
});
Redirect::to("/").into_response()
}
/// Cancel the in-flight claude turn. Coarse-grained: shells out
/// `pkill -INT claude` since there's at most one claude per container.
/// SIGINT (not SIGTERM) so claude flushes anything in-flight and emits a