1use std::sync::Arc;
8
9use super::actor::{Live, SessionActor};
10use super::types::*;
11
12fn compact_request(cmd: &str, arg: &str) -> Option<crate::engine::commands::CommandResult> {
14 match crate::engine::commands::evaluate_engine_command(cmd, arg) {
15 Some(r @ crate::engine::commands::CommandResult::Compact { .. }) => Some(r),
16 _ => None,
17 }
18}
19
20pub fn engine_command_reply(
24 cmd: &str,
25 arg: &str,
26 runtime: &mut crate::Runtime,
27 session: &mut crate::Session,
28) -> (serde_json::Value, bool) {
29 use crate::engine::commands::{handle_engine_command, CommandResult};
30 match handle_engine_command(cmd, arg, runtime) {
31 None => (serde_json::json!({ "kind": "unhandled" }), false),
32 Some(CommandResult::Quit) => (serde_json::json!({ "kind": "quit" }), false),
33 Some(CommandResult::ModelChanged {
34 model,
35 reasoning_clamped,
36 }) => {
37 session.model = runtime.model().to_string();
38 let mut text = format!("model → {}", model);
39 let mut clamp = serde_json::Value::Null;
40 if let Some(c) = reasoning_clamped {
41 session.thinking_level = runtime.thinking_level().to_string();
42 text.push_str(&format!(
43 "\nthinking → {} (clamped from {}: not supported by {})",
44 c.to.as_str(),
45 c.from.as_str(),
46 runtime.model()
47 ));
48 clamp = serde_json::json!({ "from": c.from.as_str(), "to": c.to.as_str() });
49 }
50 (
51 serde_json::json!({
52 "kind": "notice",
53 "event": "model_changed",
54 "text": text,
55 "model": runtime.model(),
56 "clamp": clamp,
57 }),
58 true,
59 )
60 }
61 Some(CommandResult::ThinkingChanged { spec }) => {
62 session.thinking_level = spec.config_value();
63 (
64 serde_json::json!({
65 "kind": "notice",
66 "event": "thinking_changed",
67 "text": format!("thinking → {}", spec.level()),
68 "level": spec.level().to_string(),
69 "config_value": spec.config_value(),
70 }),
71 true,
72 )
73 }
74 Some(CommandResult::Compact { .. }) => (serde_json::json!({ "kind": "none" }), false),
75 Some(CommandResult::Error(e)) => (serde_json::json!({ "kind": "error", "text": e }), false),
76 Some(CommandResult::Output(text)) => {
77 (serde_json::json!({ "kind": "output", "text": text }), false)
78 }
79 Some(_) => (serde_json::json!({ "kind": "none" }), false),
80 }
81}
82
83pub async fn auth_command_reply(arg: &str, runtime: &crate::Runtime) -> serde_json::Value {
87 match arg.trim() {
88 "reload" => match runtime.reload_auth().await {
89 Ok(text) => serde_json::json!({ "kind": "output", "text": text }),
90 Err(e) => serde_json::json!({
91 "kind": "error",
92 "text": format!("auth reload failed: {e}"),
93 }),
94 },
95 _ => serde_json::json!({ "kind": "error", "text": "usage: /auth reload" }),
96 }
97}
98
99impl SessionActor {
100 pub(crate) async fn engine_command(&mut self, id: u64, cmd: String, arg: String) {
106 use crate::engine::commands::CommandResult;
107 if cmd == "saveas" {
110 let value = self.session_name(&arg).await;
111 self.emit(SessionEventWire::QueryResult { id, value });
112 return;
113 }
114 if cmd == "keepwarm" {
117 let value = self.cache_keepwarm_command(&arg).await;
118 self.emit(SessionEventWire::QueryResult { id, value });
119 return;
120 }
121 if cmd == "auth" {
124 let value = auth_command_reply(&arg, &self.runtime).await;
125 self.emit(SessionEventWire::QueryResult { id, value });
126 return;
127 }
128 crate::engine::commands::prepare_engine_command(&cmd, &arg, &self.runtime).await;
129 let cancelling = cmd == "workers" && arg.trim_start().starts_with("cancel");
132 let uncancelled = if cancelling { self.uncancelled_workers() } else { Vec::new() };
133 let (value, view_changed) =
134 engine_command_reply(&cmd, &arg, &mut self.runtime, &mut self.conv.session);
135 if cancelling {
136 let after = self
137 .runtime
138 .subagent_registry()
139 .lock()
140 .unwrap_or_else(|p| p.into_inner())
141 .display_rows();
142 for (id, agent) in &cancelled_by_command(uncancelled, &after) {
143 self.note_in_main_conversation(super::actor::subagent_user_cancel_event(id, agent));
144 }
145 }
146 if view_changed {
147 self.publish_view().await;
148 let view = (**self.view.load()).clone();
152 self.emit(SessionEventWire::SettingChanged(SettingApplied {
153 id: super::wire::VIEW_CHANGED_SETTING_ID,
154 setting: cmd.clone(),
155 ok: true,
156 message: None,
157 view,
158 clamp: None,
159 }));
160 }
161 if let Some(CommandResult::Compact {
162 custom_instructions,
163 }) = compact_request(&cmd, &arg)
164 {
165 self.compact(custom_instructions, "manual").await;
166 }
167 self.emit(SessionEventWire::QueryResult { id, value });
168 }
169
170 pub(crate) fn close_command(&mut self, id: u64, arg: &str, host: bool) -> Option<EndReason> {
188 let reply = |text: String, kind: &str| SessionEventWire::QueryResult {
189 id,
190 value: serde_json::json!({ "kind": kind, "text": text }),
191 };
192 let (mut force, mut unattached) = (false, false);
193 for word in arg.split_whitespace() {
194 match word {
195 "force" if !force => force = true,
196 w if host && w == super::actor::CLOSE_UNATTACHED_ARG && !unattached => unattached = true,
197 _ => {
198 self.emit(reply("usage: /close [force]".into(), "error"));
199 return None;
200 }
201 }
202 }
203 if unattached && !self.attached.is_empty() {
204 let n = self.attached.len();
205 self.emit(reply(format!("not closed: attached by {n} client(s)"), "error"));
206 return None;
207 }
208 let live = self.runtime.is_live();
209 let workers: Vec<(String, String)> = if live {
210 let mut running: Vec<(String, String)> = self
211 .runtime
212 .subagent_registry()
213 .lock()
214 .unwrap_or_else(|p| p.into_inner())
215 .display_rows()
216 .into_iter()
217 .filter(|r| {
218 r.status == crate::runtime::subagent::SubagentStatus::Running
219 && !r.cancel_requested
220 })
221 .map(|r| (r.handle_id, r.agent_name))
222 .collect();
223 running.sort();
224 running
225 } else {
226 Vec::new()
227 };
228 let mut blockers = Vec::new();
229 if self.streaming {
230 blockers.push("a turn is running".to_string());
231 }
232 if self.compact.is_some() {
233 blockers.push("a compaction is running".to_string());
234 }
235 if !workers.is_empty() {
236 let names: Vec<String> = workers.iter().map(|(id, agent)| format!("{id} {agent}")).collect();
237 blockers.push(format!(
238 "{} worker{} running ({})",
239 workers.len(),
240 if workers.len() == 1 { " is" } else { "s are" },
241 names.join(", ")
242 ));
243 }
244 if self.driver.is_some() {
245 blockers.push("an /auto run is armed".to_string());
246 }
247 if !blockers.is_empty() && !force {
248 self.emit(reply(
249 format!(
250 "not closed: {}. Closing the session would stop {}. Wait, or use /close force.",
251 blockers.join("; "),
252 if blockers.len() == 1 { "it" } else { "them" },
253 ),
254 "error",
255 ));
256 return None;
257 }
258 if !workers.is_empty() {
259 let cancelled = self
260 .runtime
261 .subagent_registry()
262 .lock()
263 .unwrap_or_else(|p| p.into_inner())
264 .cancel_running(crate::runtime::subagent::WorkerCancelScope::All);
265 for (handle_id, agent) in workers.iter().filter(|(h, _)| cancelled.contains(h)) {
266 self.note_in_main_conversation(super::actor::subagent_close_cancel_event(handle_id, agent));
267 }
268 }
269 let session = (**self.journal_id.load()).clone();
270 let text = if self.conv.is_live() && self.conv.api_messages.is_empty() {
273 format!("closing session {session} (it never ran a turn, so there is nothing to resume)")
274 } else {
275 format!("closing session {session}; resume it with: synaps --attach --continue {session}")
276 };
277 self.emit(reply(text, "output"));
278 Some(EndReason::ClientQuit)
279 }
280
281 fn uncancelled_workers(&self) -> Vec<(String, String)> {
283 let mut workers: Vec<(String, String)> = self
284 .runtime
285 .subagent_registry()
286 .lock()
287 .unwrap_or_else(|p| p.into_inner())
288 .display_rows()
289 .into_iter()
290 .filter(|r| !r.cancel_requested)
291 .map(|r| (r.handle_id, r.agent_name))
292 .collect();
293 workers.sort();
294 workers
295 }
296
297 async fn session_name(&mut self, arg: &str) -> serde_json::Value {
300 let trimmed = arg.trim();
301 let Some(_guard) = name_lock().await else {
304 return serde_json::json!({ "kind": "error", "text": NAME_LOCK_BUSY });
305 };
306 self.persister.flush().await;
309 if trimmed.is_empty() {
310 self.conv.session.clear_name();
311 let _ = self.conv.session.save().await;
312 self.sync_name();
313 self.emit_conversation();
314 return serde_json::json!({
315 "kind": "output", "text": "session name cleared", "name": serde_json::Value::Null,
316 });
317 }
318 match self.conv.session.set_name(trimmed) {
319 Ok(()) => {
320 let _ = self.conv.session.save().await;
321 self.sync_name();
322 self.emit_conversation();
323 serde_json::json!({
324 "kind": "output",
325 "text": format!("session named '{}'", trimmed),
326 "name": trimmed,
327 })
328 }
329 Err(e) => serde_json::json!({ "kind": "error", "text": e.to_string() }),
330 }
331 }
332
333 pub(crate) async fn submit_prepared(
337 &mut self,
338 messages: Vec<crate::SharedMessage>,
339 user_text: Option<String>,
340 ) {
341 if self.streaming {
342 self.emit(SessionEventWire::SystemNotice(
343 "cannot load a skill while streaming".into(),
344 ));
345 return;
346 }
347 self.conv.api_messages.extend(messages);
348 if let Some(text) = user_text {
349 self.conv
350 .api_messages
351 .push(Arc::new(serde_json::json!({"role": "user", "content": text})));
352 }
353 self.start_turn(TurnTrigger::PluginCommand, None).await;
354 }
355
356 pub(crate) async fn plugin_command(&mut self, id: u64, plugin: String, name: String, arg: String) {
360 let value = match Self::find_plugin_command(&plugin, &name) {
361 None => serde_json::json!({
362 "kind": "error",
363 "text": format!("unknown plugin command /{plugin}:{name}"),
364 }),
365 Some(cmd) => match crate::skills::commands::execute_plugin_command_with_tools(
366 &cmd,
367 &arg,
368 self.runtime.tools_shared(),
369 self.runtime.command_chat(),
374 )
375 .await
376 {
377 Ok(output) => serde_json::json!({
378 "kind": "plugin_output",
379 "status": output.status,
380 "stdout": output.stdout,
381 "stderr": output.stderr,
382 }),
383 Err(e) => serde_json::json!({ "kind": "error", "text": e.to_string() }),
384 },
385 };
386 self.emit(SessionEventWire::QueryResult { id, value });
387 }
388
389 fn find_plugin_command(
390 plugin: &str,
391 name: &str,
392 ) -> Option<Arc<crate::skills::registry::RegisteredPluginCommand>> {
393 let host = crate::host::EngineHost::current()?;
394 match host.command_registry().resolve(&format!("{plugin}:{name}")) {
395 crate::skills::registry::Resolution::PluginCommand(c) => Some(c),
396 _ => None,
397 }
398 }
399
400 pub(crate) async fn resume(&mut self, id: u64, query: String) {
404 if self.streaming {
405 self.emit(SessionEventWire::QueryResult {
406 id,
407 value: serde_json::json!({ "kind": "error", "text": "cannot resume while streaming" }),
408 });
409 return;
410 }
411 self.persister.flush().await;
414 let session = match crate::resolve_session(&query) {
415 Ok(s) => s,
416 Err(e) => {
417 self.emit(SessionEventWire::QueryResult {
418 id,
419 value: serde_json::json!({ "kind": "error", "text": e.to_string() }),
420 });
421 return;
422 }
423 };
424 let old_id = self.conv.session.id.clone();
425 let new_id = session.id.clone();
426 let new_lock = if new_id == old_id {
432 None } else {
434 match Self::try_lock_session(&new_id) {
435 Ok(lock) => Some(lock),
436 Err(e @ agent_core::session_lock::SessionLockError::Held { .. })
437 | Err(e @ agent_core::session_lock::SessionLockError::CompactedInto { .. }) => {
438 self.emit(SessionEventWire::QueryResult {
439 id,
440 value: serde_json::json!({
441 "kind": "error",
442 "text": format!("cannot resume: {e}"),
443 }),
444 });
445 return;
446 }
447 Err(e) => {
448 tracing::warn!(session = %new_id, "resume: session lock: {e}");
451 None
452 }
453 }
454 };
455 self.cache_keepwarm_conversation_replaced();
458 self.runtime.set_model(session.model.clone());
459 let clamp_notice = self
462 .runtime
463 .restore_session_reasoning(&session.thinking_level)
464 .map(|clamp| {
465 format!(
466 "thinking → {} (clamped from {}: not supported by {})",
467 clamp.to.as_str(),
468 clamp.from.as_str(),
469 self.runtime.model()
470 )
471 });
472 if let Some(ref sp) = session.system_prompt {
473 self.runtime.set_system_prompt(sp.clone());
474 }
475 self.save().await;
476 let via = if crate::chain::load_chain(&query).is_ok() {
477 Some(format!("chain '{}'", query))
478 } else if crate::find_session_by_name(&query).is_ok() {
479 Some(format!("name '{}'", query))
480 } else {
481 None
482 };
483 if new_id != old_id {
484 self.session_lock = new_lock;
486 }
487 self.conv = Live::new(crate::engine::session::ConversationState::from_resumed(session));
488 if self.config.persist && self.session_lock.is_some() {
490 crate::engine::setup::recover_turn_draft(&mut self.conv).await;
491 }
492 if clamp_notice.is_some() {
493 self.conv.session.thinking_level = self.runtime.thinking_level().to_string();
495 }
496 self.consecutive_auto_turns = 0;
497 self.runtime.begin_conversation(Some(new_id.clone()));
498 self.set_journal_id(new_id.clone());
499 self.publish_view().await;
500 self.emit(SessionEventWire::Resumed {
501 id,
502 old_id,
503 new_id,
504 via,
505 clamp_notice,
506 });
507 self.emit_conversation();
508 }
509
510 #[allow(dead_code)]
513 pub(crate) fn emit_subagent_rows(&mut self) {
514 let rows = self
515 .runtime
516 .subagent_registry()
517 .lock()
518 .unwrap_or_else(|p| p.into_inner())
519 .display_rows();
520 self.emit(SessionEventWire::SubagentRows(rows));
521 }
522
523 #[allow(dead_code)]
525 pub(crate) fn has_live_subagents(&self) -> bool {
526 self.runtime
527 .subagent_registry()
528 .lock()
529 .unwrap_or_else(|p| p.into_inner())
530 .display_rows()
531 .iter()
532 .any(|r| matches!(r.status, crate::runtime::subagent::SubagentStatus::Running))
533 }
534}
535
536fn cancelled_by_command(
540 before: Vec<(String, String)>,
541 after: &[crate::runtime::subagent::SubagentDisplayRow],
542) -> Vec<(String, String)> {
543 before
544 .into_iter()
545 .filter(|(id, _)| after.iter().any(|r| &r.handle_id == id && r.cancel_requested))
546 .collect()
547}
548
549#[cfg(test)]
550mod tests {
551 use crate::Runtime;
554
555 fn row(id: &str, cancel_requested: bool) -> crate::runtime::subagent::SubagentDisplayRow {
556 serde_json::from_value(serde_json::json!({
557 "subagent_id": 0, "agent_name": "a", "status": "running",
558 "cancel_requested": cancel_requested, "elapsed_secs": 0.0,
559 "finished_elapsed": null, "handle_id": id,
560 }))
561 .unwrap()
562 }
563
564 #[test]
568 fn the_cancel_note_names_only_workers_the_command_flagged() {
569 let before: Vec<(String, String)> = ["sa_1", "sa_2", "sa_3"]
570 .iter()
571 .map(|id| (id.to_string(), "a".to_string()))
572 .collect();
573 let after = [row("sa_1", true), row("sa_3", false)];
575 let named: Vec<String> =
576 super::cancelled_by_command(before, &after).into_iter().map(|(id, _)| id).collect();
577 assert_eq!(named, ["sa_1"]);
578 }
579
580 #[tokio::test]
581 async fn restore_session_reasoning_keeps_explicit_provenance() {
582 let mut runtime = Runtime::new().await.unwrap();
583 runtime.set_model("openai-codex/gpt-5.6-sol".to_string());
584 let clamp = runtime.restore_session_reasoning("ultra");
585 assert!(clamp.is_none());
586 assert_eq!(
587 runtime.reasoning_level(),
588 agent_core::reasoning::ReasoningLevel::Ultra
589 );
590 assert!(runtime.is_reasoning_explicit());
591 runtime.set_model("openai-codex/gpt-5.6-terra".to_string());
592 assert_eq!(
593 runtime.reasoning_level(),
594 agent_core::reasoning::ReasoningLevel::Ultra
595 );
596 }
597
598 #[tokio::test]
599 async fn restore_session_reasoning_clamps_unsupported_level() {
600 let mut runtime = Runtime::new().await.unwrap();
601 runtime.set_model("xai-auth/grok-4.6".to_string());
602 let clamp = runtime.restore_session_reasoning("xhigh");
603 assert!(clamp.is_some());
604 assert_eq!(
605 runtime.reasoning_level(),
606 agent_core::reasoning::ReasoningLevel::High
607 );
608 assert!(runtime.is_reasoning_explicit());
609 }
610}
611
612#[cfg(test)]
613mod auth_command_tests {
614 use super::auth_command_reply;
615
616 #[tokio::test]
617 async fn auth_command_usage_and_token_free_error() {
618 let rt = crate::Runtime::new_headless(); let v = auth_command_reply("bogus", &rt).await;
620 assert_eq!(v["kind"], "error");
621 assert!(v["text"].as_str().unwrap().contains("usage: /auth reload"));
622 let v = auth_command_reply(" reload ", &rt).await;
623 let t = v["text"].as_str().unwrap();
624 assert_eq!(v["kind"], "error");
625 assert!(t.contains("nothing to reload"), "{t}");
626 assert!(!t.contains("test-token"), "{t}");
627 }
628
629 #[test]
630 fn auth_is_a_builtin_command() {
631 assert!(crate::skills::BUILTIN_COMMANDS.contains(&"auth"));
632 }
633}
634
635static NAME_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
643
644pub const NAME_LOCK_WAIT: std::time::Duration = std::time::Duration::from_secs(3);
648
649async fn name_lock() -> Option<tokio::sync::MutexGuard<'static, ()>> {
650 tokio::time::timeout(NAME_LOCK_WAIT, NAME_LOCK.lock()).await.ok()
651}
652
653const NAME_LOCK_BUSY: &str = "another session is saving a rename; retry in a moment";
654
655#[doc(hidden)]
657pub async fn hold_name_lock_for_tests() -> tokio::sync::MutexGuard<'static, ()> {
658 NAME_LOCK.lock().await
659}
660
661#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
664pub(crate) struct RenameArgs {
665 pub name: Option<String>,
666 #[serde(
669 default,
670 deserialize_with = "crate::session::wire::present_option",
671 skip_serializing_if = "Option::is_none"
672 )]
673 pub expect: Option<Option<String>>,
674}
675
676fn rename_error(code: &str, text: impl Into<String>) -> serde_json::Value {
677 serde_json::json!({ "kind": "error", "code": code, "text": text.into() })
678}
679
680fn rename_ok(name: &Option<String>, in_memory_only: bool) -> serde_json::Value {
681 let text = match (name, in_memory_only) {
682 (Some(n), false) => format!("session named '{n}'"),
683 (None, false) => "session name cleared".to_string(),
684 (Some(n), true) => format!("session named '{n}' (in memory only: this session is not saved)"),
685 (None, true) => "session name cleared (in memory only: this session is not saved)".to_string(),
686 };
687 serde_json::json!({ "kind": "output", "text": text, "name": name, "in_memory_only": in_memory_only })
688}
689
690fn name_error_reply(e: agent_core::session::NameError) -> serde_json::Value {
691 let mut v = rename_error(e.code(), e.to_string());
692 if let agent_core::session::NameError::Taken { by, .. } = &e {
693 v["taken_by"] = serde_json::json!(by);
694 }
695 if matches!(e, agent_core::session::NameError::Io(_)) {
696 v["code"] = serde_json::json!("failed");
697 }
698 v
699}
700
701impl SessionActor {
702 pub(crate) async fn host_rename(&mut self, arg: &str) -> serde_json::Value {
709 let Ok(args) = serde_json::from_str::<RenameArgs>(arg) else {
710 return rename_error(
711 "failed",
712 "rename is the daemon's control command (`synaps daemon rename`); use /saveas",
713 );
714 };
715 let want = args.name.as_deref().map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
716 if let Some(n) = &want {
717 if let Err(e) = agent_core::session::validate_name(n) {
718 return rename_error("invalid", e);
719 }
720 }
721 let Some(_guard) = name_lock().await else {
722 return rename_error("busy", NAME_LOCK_BUSY);
723 };
724 let current: Option<String> =
725 if self.is_parked() { (**self.name.load()).clone() } else { self.conv.session.name.clone() };
726 if let Some(expected) = &args.expect {
727 if *expected != current {
728 let text = match (¤t, expected) {
729 (Some(c), None) => format!("session is named '{c}'; rename never replaces a name without --force"),
730 (Some(c), Some(e)) => format!("session is named '{c}', not '{e}'; not renamed"),
731 (None, _) => "session has no name any more; not renamed".to_string(),
732 };
733 let mut v = rename_error("named", text);
734 v["current_name"] = serde_json::json!(current);
735 return v;
736 }
737 }
738 if self.is_parked() {
739 self.rename_parked(want, args.expect).await
741 } else if want == current {
742 rename_ok(&want, !self.config.persist)
743 } else {
744 self.rename_live(want).await
745 }
746 }
747
748 async fn rename_live(&mut self, want: Option<String>) -> serde_json::Value {
749 if let Some(n) = &want {
750 if let Err(e) = self.conv.session.check_name(n) {
752 return name_error_reply(e);
753 }
754 }
755 if !self.config.persist {
756 self.conv.session.name = want.clone();
757 self.sync_name();
758 self.emit_conversation();
759 return rename_ok(&want, true);
760 }
761 if self.session_lock.is_none() {
762 return rename_error(
763 "locked",
764 "this daemon does not hold the session's journal lock (it may be open in another process); not renamed",
765 );
766 }
767 self.persister.flush().await;
769 let dir = agent_core::session_lock::sessions_dir();
770 let id = self.conv.session.id.clone();
771 let mtimes = agent_core::session_catalog::SessionMtimes::capture(&dir, &id);
772 let old = std::mem::replace(&mut self.conv.session.name, want.clone());
773 if let Err(e) = self.conv.session.save_durable().await {
774 self.conv.session.name = old;
775 return rename_error("failed", format!("rename not saved (rolled back): {e}"));
776 }
777 mtimes.restore(&dir, &id);
778 self.sync_name();
779 self.emit_conversation();
780 rename_ok(&want, false)
781 }
782
783 async fn rename_parked(&mut self, want: Option<String>, expect: Option<Option<String>>) -> serde_json::Value {
787 use agent_core::session_catalog::RenameError;
788 let dir = agent_core::session_lock::sessions_dir();
789 let id = (**self.journal_id.load()).clone();
790 let name = want.clone();
791 let res = tokio::task::spawn_blocking(move || {
792 let expect = expect.as_ref().map(|e| e.as_deref());
793 agent_core::session_catalog::rename_past_session_if(&dir, &id, name.as_deref(), expect)
794 })
795 .await;
796 match res {
797 Ok(Ok(())) => {}
798 Ok(Err(e)) => {
799 let code = match &e {
800 RenameError::InvalidName(_) => "invalid",
801 RenameError::NameTaken { .. } => "taken",
802 RenameError::ChainConflict => "chain_conflict",
803 RenameError::Live { .. } => "locked",
804 RenameError::NameMismatch { .. } => "named",
805 _ => "failed",
806 };
807 let mut v = rename_error(code, e.to_string());
808 if let RenameError::NameTaken { other_id } = &e {
809 v["taken_by"] = serde_json::json!(other_id);
810 }
811 if let RenameError::NameMismatch { current } = e {
812 v["current_name"] = serde_json::json!(current);
813 self.mirror_name(current);
814 }
815 return v;
816 }
817 Err(e) => return rename_error("failed", format!("rename failed: {e}")),
818 }
819 self.mirror_name(want.clone());
820 rename_ok(&want, false)
821 }
822}