diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index ce3f8f0b83..af1776fefb 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -640,14 +640,7 @@ pub fn model_token_limit(model: &str) -> Option { max_output_tokens: 16_384, context_window_tokens: 256_000, }), - // Hotfix: Unknown models get conservative defaults to avoid crashes. - // Uses the minimum of known supported models: max_output_tokens: 16_384, - // context_window_tokens: 131_072. This may under-utilize the model's - // actual capabilities but hopefully ensures safer operation. - _ => Some(ModelTokenLimit { - max_output_tokens: 16_384, - context_window_tokens: 131_072, - }), + _ => None, } } diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index 7fbf636717..d5291b8eae 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -1857,6 +1857,7 @@ mod tests { delta: super::ChunkDelta { content: None, reasoning_content: Some("think".to_string()), + thinking: None, tool_calls: Vec::new(), }, finish_reason: None, @@ -1873,6 +1874,7 @@ mod tests { delta: super::ChunkDelta { content: Some(" answer".to_string()), reasoning_content: None, + thinking: None, tool_calls: Vec::new(), }, finish_reason: Some("stop".to_string()), diff --git a/rust/crates/api/tests/client_integration.rs b/rust/crates/api/tests/client_integration.rs index 512e34615a..15959e7191 100644 --- a/rust/crates/api/tests/client_integration.rs +++ b/rust/crates/api/tests/client_integration.rs @@ -82,7 +82,7 @@ async fn send_message_posts_json_and_parses_response() { ); assert_eq!( request.headers.get("user-agent").map(String::as_str), - Some("claude-code/0.1.0") + Some("claude-code/0.1.3") ); assert_eq!( request.headers.get("anthropic-beta").map(String::as_str), diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index de9af6c3e4..d431ad9fc3 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -1180,6 +1180,9 @@ pub enum SlashCommand { count: Option, }, Unknown(String), + Team { + action: Option, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1277,6 +1280,7 @@ impl SlashCommand { Self::Tag { .. } => "/tag", Self::OutputStyle { .. } => "/output-style", Self::AddDir { .. } => "/add-dir", + Self::Team { .. } => "/team", Self::Sandbox => "/sandbox", Self::Mcp { .. } => "/mcp", Self::Export { .. } => "/export", @@ -4312,6 +4316,7 @@ pub fn handle_slash_command( | SlashCommand::OutputStyle { .. } | SlashCommand::AddDir { .. } | SlashCommand::History { .. } + | SlashCommand::Team { .. } | SlashCommand::Unknown(_) => None, } } diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index c1ec17d45d..362a0b5436 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -1230,4 +1230,44 @@ mod tests { ); fs::remove_dir_all(base).expect("temp dir should clean up"); } + + /// #160 regression: store-level list_sessions/session_exists/delete_session + /// lifecycle works end-to-end. + #[test] + fn session_store_lifecycle_regression_160() { + // given + let base = temp_dir(); + fs::create_dir_all(&base).expect("base dir should exist"); + let store = SessionStore::from_cwd(&base).expect("store should build"); + let session = persist_session_via_store(&store, "160 regression test"); + + // when/then — session exists and is listed before deletion + assert!( + !store.list_sessions().expect("list").is_empty(), + "store should have at least one session" + ); + assert!( + store.session_exists(&session.session_id), + "session should exist before deletion" + ); + + // when — delete the session + let deleted = store + .delete_session(&session.session_id) + .expect("delete should succeed"); + + // then — session is gone + assert_eq!(deleted.id, session.session_id); + assert!(!deleted.path.exists(), "session file should be removed"); + assert!( + !store.session_exists(&session.session_id), + "session should not exist after deletion" + ); + assert!( + store.list_sessions().expect("list").is_empty(), + "store should have no sessions after deletion" + ); + + fs::remove_dir_all(base).expect("temp dir should clean up"); + } } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index a3e04434b7..fc9c071a09 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -274,6 +274,8 @@ fn classify_error_kind(message: &str) -> &'static str { "no_managed_sessions" } else if message.contains("unsupported ACP invocation") { "unsupported_acp_invocation" + } else if message.contains("unsupported skills action") { + "unsupported_skills_action" } else if message.contains("unrecognized argument") || message.contains("unknown option") { "cli_parse" } else if message.contains("invalid model syntax") { @@ -877,6 +879,35 @@ fn parse_args(args: &[String]) -> Result { } if wants_help { + // #684: --help before subcommand should still route to subcommand-specific + // help when the subcommand is one of the local-help-topic commands. + if let Some(action) = parse_local_help_action(&rest, output_format) { + return action; + } + // When --help was consumed before the subcommand, rest has no help flag. + // If rest is a simple local-help subcommand with no extra args, route there. + if !rest.is_empty() && rest[1..].iter().all(|a| is_help_flag(a)) { + let topic = match rest[0].as_str() { + "status" => Some(LocalHelpTopic::Status), + "sandbox" => Some(LocalHelpTopic::Sandbox), + "doctor" => Some(LocalHelpTopic::Doctor), + "acp" => Some(LocalHelpTopic::Acp), + "init" => Some(LocalHelpTopic::Init), + "state" => Some(LocalHelpTopic::State), + "export" => Some(LocalHelpTopic::Export), + "version" => Some(LocalHelpTopic::Version), + "system-prompt" => Some(LocalHelpTopic::SystemPrompt), + "dump-manifests" => Some(LocalHelpTopic::DumpManifests), + "bootstrap-plan" => Some(LocalHelpTopic::BootstrapPlan), + _ => None, + }; + if let Some(topic) = topic { + return Ok(CliAction::HelpTopic { + topic, + output_format, + }); + } + } return Ok(CliAction::Help { output_format }); } @@ -1020,6 +1051,14 @@ fn parse_args(args: &[String]) -> Result { ), "skills" => { let args = join_optional_args(&rest[1..]); + if let Some(action) = args.as_deref() { + let first_word = action.split_whitespace().next().unwrap_or(action); + if matches!(first_word, "remove" | "add" | "uninstall" | "delete") { + return Err(format!( + "unsupported skills action: {first_word}. Supported actions: list, install , help, or [args]" + )); + } + } match classify_skills_slash_command(args.as_deref()) { SkillSlashDispatch::Invoke(prompt) => Ok(CliAction::Prompt { prompt, @@ -1117,7 +1156,10 @@ fn parse_local_help_action( rest: &[String], output_format: CliOutputFormat, ) -> Option> { - if rest.len() != 2 || !is_help_flag(&rest[1]) { + if rest.is_empty() { + return None; + } + if !rest.iter().any(|a| is_help_flag(a)) { return None; } @@ -1126,10 +1168,6 @@ fn parse_local_help_action( "sandbox" => LocalHelpTopic::Sandbox, "doctor" => LocalHelpTopic::Doctor, "acp" => LocalHelpTopic::Acp, - // #141: add the subcommands that were previously falling back - // to global help (init/state/export/version) or erroring out - // (system-prompt/dump-manifests) or printing their primary - // output instead of help text (bootstrap-plan). "init" => LocalHelpTopic::Init, "state" => LocalHelpTopic::State, "export" => LocalHelpTopic::Export, @@ -1139,6 +1177,10 @@ fn parse_local_help_action( "bootstrap-plan" => LocalHelpTopic::BootstrapPlan, _ => return None, }; + let has_non_help = rest[1..].iter().any(|a| !is_help_flag(a)); + if has_non_help { + return None; + } Some(Ok(CliAction::HelpTopic { topic, output_format, @@ -1172,8 +1214,9 @@ fn parse_single_word_command_alias( if is_diagnostic && rest.len() > 1 { // Diagnostic verb with trailing args: reject unrecognized suffix - if is_help_flag(&rest[1]) && rest.len() == 2 { - // "doctor --help" is valid, routed to parse_local_help_action() instead + let all_extra_are_help = rest[1..].iter().all(|a| is_help_flag(a)); + if all_extra_are_help { + // "doctor --help -h" is valid, routed to parse_local_help_action() instead return None; } // Unrecognized suffix like "--json" @@ -4209,7 +4252,8 @@ fn run_resume_command( | SlashCommand::Ide { .. } | SlashCommand::Tag { .. } | SlashCommand::OutputStyle { .. } - | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()), + | SlashCommand::AddDir { .. } + | SlashCommand::Team { .. } => Err("unsupported resumed slash command".into()), } } @@ -5456,7 +5500,8 @@ impl LiveCli { | SlashCommand::Ide { .. } | SlashCommand::Tag { .. } | SlashCommand::OutputStyle { .. } - | SlashCommand::AddDir { .. } => { + | SlashCommand::AddDir { .. } + | SlashCommand::Team { .. } => { let cmd_name = command.slash_name(); eprintln!("{cmd_name} is not yet implemented in this build."); false @@ -12711,6 +12756,23 @@ mod tests { assert!(error.contains("skills")); } + #[test] + fn unsupported_skills_actions_return_typed_error_683() { + for action in ["remove", "add", "uninstall", "delete"] { + let error = parse_args(&["skills".to_string(), action.to_string()]) + .expect_err(&format!("skills {action} should error")); + assert!( + error.contains("unsupported skills action"), + "skills {action} should contain 'unsupported skills action', got: {error}" + ); + assert_eq!( + classify_error_kind(&error), + "unsupported_skills_action", + "skills {action} should classify as unsupported_skills_action, got: {error}" + ); + } + } + #[test] fn typoed_status_subcommand_returns_did_you_mean_error() { let error = parse_args(&["statuss".to_string()]).expect_err("statuss should error");