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
+27 -10
View File
@@ -303,6 +303,31 @@ fn main() {
}
}
// Handle state management commands locally — these are pure file operations
// that don't need a daemon, avoiding an unnecessary daemon startup that
// would lack runtime config like session_name.
if let Some(result) = native::state::dispatch_state_command(&cmd) {
let action = cmd.get("action").and_then(|v| v.as_str());
let resp = match result {
Ok(data) => connection::Response {
success: true,
data: Some(data),
error: None,
},
Err(e) => connection::Response {
success: false,
data: None,
error: Some(e),
},
};
let output_opts = OutputOptions::from_flags(&flags);
output::print_response_with_opts(&resp, action, &output_opts);
if !resp.success {
exit(1);
}
return;
}
let daemon_opts = DaemonOptions {
headed: flags.headed,
debug: flags.debug,
@@ -744,11 +769,7 @@ fn main() {
return;
}
let output_opts = OutputOptions {
json: flags.json,
content_boundaries: flags.content_boundaries,
max_output: flags.max_output,
};
let output_opts = OutputOptions::from_flags(&flags);
match send_command(cmd.clone(), &flags.session) {
Ok(resp) => {
@@ -863,11 +884,7 @@ fn run_batch(flags: &Flags, bail: bool) {
return;
}
let output_opts = OutputOptions {
json: flags.json,
content_boundaries: flags.content_boundaries,
max_output: flags.max_output,
};
let output_opts = OutputOptions::from_flags(flags);
let mut results: Vec<serde_json::Value> = Vec::new();
let mut had_error = false;