force fresh session: ↻ new session button + /new-session
bus carries a one-shot AtomicBool armed by POST /api/new-session (or the /new-session slash command). next turn drops --continue, starting a fresh claude session; the flag clears automatically so subsequent turns resume normal behavior. /compact still always uses --continue — compacting a non-existent session is a no-op anyway. per-agent page grows an ↻ new session button next to the cancel-turn one (always visible, amber, confirms before posting since dropping --continue context isn't reversible). slash-command surface picks up /new-session for parity with the button. note row emitted on the live feed both at arm- time and again when the turn actually consumes the flag, so the operator can confirm it landed.
This commit is contained in:
parent
14aa7c7acc
commit
034b4fde10
6 changed files with 102 additions and 6 deletions
|
|
@ -9,6 +9,7 @@
|
|||
//! showing "connecting…" until the first event arrives.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
|
|
@ -189,6 +190,13 @@ pub struct Bus {
|
|||
/// Model name passed to `claude --model`. Default `haiku`; the
|
||||
/// operator can override at runtime via `POST /api/model`.
|
||||
model: Arc<Mutex<String>>,
|
||||
/// One-shot: next `run_claude` call drops `--continue`, starting
|
||||
/// a fresh claude session. Set by `POST /api/new-session` from
|
||||
/// the per-agent web UI; consumed (cleared back to false) by the
|
||||
/// next turn. Subsequent turns resume normal `--continue`
|
||||
/// behavior. Atomic so the consumer can take-and-clear without a
|
||||
/// lock.
|
||||
skip_continue_once: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Bus {
|
||||
|
|
@ -213,9 +221,24 @@ impl Bus {
|
|||
store,
|
||||
state: Arc::new(Mutex::new((TurnState::Idle, now_unix()))),
|
||||
model: Arc::new(Mutex::new(initial_model)),
|
||||
skip_continue_once: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Arm the one-shot: the next claude invocation will run without
|
||||
/// `--continue`, dropping any prior session context. Idempotent
|
||||
/// — calling twice in a row before the next turn still consumes
|
||||
/// to a single fresh-start.
|
||||
pub fn request_new_session(&self) {
|
||||
self.skip_continue_once.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Take + clear the one-shot. Returns true iff the caller should
|
||||
/// run claude without `--continue` for this turn.
|
||||
pub fn take_skip_continue(&self) -> bool {
|
||||
self.skip_continue_once.swap(false, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Currently-selected claude model name. Read on every turn so a
|
||||
/// `/model <name>` flip takes effect on the next turn.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -228,6 +228,18 @@ async fn run_claude(
|
|||
mode: ClaudeMode,
|
||||
) -> Result<bool> {
|
||||
let model = bus.model();
|
||||
// /compact must always run against the existing session — otherwise
|
||||
// there's nothing to compact. Only normal turns honor the
|
||||
// operator's "new session" one-shot flag.
|
||||
let resume = match mode {
|
||||
ClaudeMode::Turn => !bus.take_skip_continue(),
|
||||
ClaudeMode::Compact => true,
|
||||
};
|
||||
if !resume {
|
||||
bus.emit(LiveEvent::Note(
|
||||
"fresh session (--continue suppressed for this turn)".into(),
|
||||
));
|
||||
}
|
||||
let mut cmd = Command::new("claude");
|
||||
cmd.arg("--print")
|
||||
.arg("--verbose")
|
||||
|
|
@ -235,9 +247,11 @@ async fn run_claude(
|
|||
.arg("stream-json")
|
||||
.arg("--model")
|
||||
.arg(&model)
|
||||
.arg("--continue")
|
||||
.arg("--settings")
|
||||
.arg(settings);
|
||||
if resume {
|
||||
cmd.arg("--continue");
|
||||
}
|
||||
if let Some(p) = system_prompt {
|
||||
cmd.arg("--system-prompt-file").arg(p);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ pub async fn serve(
|
|||
.route("/api/cancel", post(post_cancel_turn))
|
||||
.route("/api/compact", post(post_compact))
|
||||
.route("/api/model", post(post_set_model))
|
||||
.route("/api/new-session", post(post_new_session))
|
||||
.with_state(state);
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = bind_with_retry(addr, "web UI").await?;
|
||||
|
|
@ -427,6 +428,21 @@ async fn post_compact(State(state): State<AppState>) -> Response {
|
|||
/// final result row. Emits a Note so the operator sees the cancel
|
||||
/// landed; the actual state transition back to `idle` happens when
|
||||
/// `run_claude` wakes up and the harness emits `TurnEnd`.
|
||||
/// Arm a one-shot: the next claude turn drops `--continue`, starting a
|
||||
/// fresh session. Subsequent turns resume normal `--continue`
|
||||
/// behavior. Idempotent before the next turn fires — calling twice
|
||||
/// still results in a single fresh start. Useful when the
|
||||
/// session-resume context is poisoned (claude went off the rails,
|
||||
/// hit an unrecoverable refusal, etc.) and a full reset is cheaper
|
||||
/// than asking claude to forget mid-stream.
|
||||
async fn post_new_session(State(state): State<AppState>) -> Response {
|
||||
state.bus.request_new_session();
|
||||
state.bus.emit(crate::events::LiveEvent::Note(
|
||||
"operator: new session armed — next turn runs without --continue".into(),
|
||||
));
|
||||
Redirect::to("/").into_response()
|
||||
}
|
||||
|
||||
async fn post_cancel_turn(State(state): State<AppState>) -> Response {
|
||||
let out = tokio::process::Command::new("pkill")
|
||||
.args(["-INT", "claude"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue