fix: prevent state commands from starting daemon without session_name (#677) (#964)

* fix: prevent state commands from starting daemon without session_name
   (#677)

  State management commands (state_list, state_show, state_clear,
  state_clean, state_rename) are pure file operations that don't need a
  running daemon. Previously, these commands would trigger daemon
  startup
  via ensure_daemon(), and if AGENT_BROWSER_SESSION_NAME was exported
  after the first command (e.g. `state clear --all`), the daemon would
  start without session_name. Subsequent open/close commands would
  reuse
  that daemon, causing close to skip state persistence entirely.

  Fix: execute state management commands locally in the CLI process
  before
  ensure_daemon() is called. This is done via a new
  dispatch_state_command() function in state.rs that centralizes the
  command routing, used by both the CLI (local path) and the daemon
  (batch/IPC path).

  Also:
  - Add OutputOptions::from_flags() helper to deduplicate construction
  - Add unit tests for dispatch_state_command routing and error
  handling

* style: fix fmt and clippy warnings

- Remove redundant closure in dispatch_state_command (clippy::redundant_closure)
- Remove needless borrow in run_batch (clippy::needless_borrow)
- Fix trailing blank lines (rustfmt)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jin.2
2026-03-23 08:32:14 -05:00
committed by GitHub
co-authored by Claude Opus 4.6 hyunjinee
parent d374e413be
commit 9c0955ca99
4 changed files with 117 additions and 49 deletions
+4 -39
View File
@@ -947,11 +947,10 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"errors" => handle_errors(state).await,
"state_save" => handle_state_save(cmd, state).await,
"state_load" => handle_state_load(cmd, state).await,
"state_list" => handle_state_list().await,
"state_show" => handle_state_show(cmd).await,
"state_clear" => handle_state_clear(cmd).await,
"state_clean" => handle_state_clean(cmd).await,
"state_rename" => handle_state_rename(cmd).await,
"state_list" | "state_show" | "state_clear" | "state_clean" | "state_rename" => {
state::dispatch_state_command(cmd)
.expect("dispatch_state_command must handle all state_* actions matched here")
}
"trace_start" => handle_trace_start(state).await,
"trace_stop" => handle_trace_stop(cmd, state).await,
"profiler_start" => handle_profiler_start(cmd, state).await,
@@ -2671,40 +2670,6 @@ async fn handle_state_load(cmd: &Value, state: &DaemonState) -> Result<Value, St
Ok(json!({ "loaded": true, "path": path }))
}
async fn handle_state_list() -> Result<Value, String> {
state::state_list()
}
async fn handle_state_show(cmd: &Value) -> Result<Value, String> {
let path = cmd
.get("path")
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;
state::state_show(path)
}
async fn handle_state_clear(cmd: &Value) -> Result<Value, String> {
let path = cmd.get("path").and_then(|v| v.as_str());
state::state_clear(path)
}
async fn handle_state_clean(cmd: &Value) -> Result<Value, String> {
let days = cmd.get("days").and_then(|v| v.as_u64()).unwrap_or(30);
state::state_clean(days)
}
async fn handle_state_rename(cmd: &Value) -> Result<Value, String> {
let path = cmd
.get("path")
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;
let name = cmd
.get("name")
.and_then(|v| v.as_str())
.ok_or("Missing 'name' parameter")?;
state::state_rename(path, name)
}
// ---------------------------------------------------------------------------
// Phase 6 handlers
// ---------------------------------------------------------------------------