Skip to content

Commit 05eb7a4

Browse files
echobtfactorydroid
andauthored
fix: batch merge of bug fixes (PRs #281, #284, #286, #287, #288, #289) (#364)
* fix: batch merge of bug fixes (PR #287) - issues #2743, #2744, #2745, #2746, #2749, #2750, #2751, #2752, #2754, #2755 Fixes: - #2743: Use tokio::select! for ordered stdout/stderr interleaving - #2744: Add CWD validation before command execution - #2745: Implement Retry-After header parsing and respect in retry logic - #2746: Add read_timeout to HTTP client to prevent hangs on truncated responses - #2749: Enable cookie_store for cookie persistence across redirects - #2750: Add --method flag for HEAD/GET method selection in scrape command - #2751: Add comprehensive base64 validation for import command - #2752: Add EmptyResponse event type for malformed API responses - #2754: Add atomic_write functions using write-temp-then-rename pattern - #2755: Add poll_with_backoff method to prevent CPU spin on non-blocking stdin * fix: batch merge of bug fixes (PR #286) - issues #2805, #2806, #2808, #2811, #2817 Fixes: - #2805: Install panic hook to track background thread panics and propagate to main thread exit code - #2806: Add /reload-config command to reload configuration from disk without restart - #2808: Use bash instead of sh when bash-specific syntax is detected (e.g., [[, <(), **, source) - #2811: Add ANSI code stripping utilities for piped/redirected output - #2817: /clear command now clears terminal scrollback buffer for privacy * fix: batch merge of bug fixes (PR #281) - issues #2700, #2702, #2703, #2704, #2706, #2708, #2710, #2711, #2712, #2714 Fixes: - #2700: Piped Input Corrupts Terminal Raw Mode State - Check TTY before TUI mode - #2702: Panic Messages Don't Suggest RUST_BACKTRACE - Add panic hooks with helpful debugging tips - #2703: Missing --frequency-penalty and --presence-penalty flags - #2704: Missing --stop flag for stop sequences - #2706: Running with sudo Creates Root-Owned Config Files - Use original user's home - #2708: Worker Threads Not Properly Joined During Shutdown - Add cleanup method - #2710: --timeout Doesn't Apply to TCP Connection Phase - Add connect_timeout - #2711: Missing --logprobs flag for token probabilities - #2712: Missing --n flag for multiple completions - #2714: Missing --best-of flag for best completion selection * fix: batch merge of bug fixes (PR #284) - issues #2818, #2820, #2821, #2822, #2823, #2824, #2829, #2831, #2832, #2834 Fixes: - #2818: Add allow_hyphen_values to run/exec prompts so dash-prefixed values work - #2820: File descriptor handling follows Rust ownership/Drop patterns (documented) - #2821: Use random temp directory names to prevent symlink attacks - #2822: Document modal stack escape key routing behavior - #2823: Fix TUI copy action (Ctrl+Shift+C) to actually perform clipboard copy - #2824: Flush stdout after JSON output when piped for proper streaming - #2829: Handle virtual filesystem (procfs/sysfs) files that report 0 size - #2831: Add --yes/-y flag as alias for --force in mcp remove command - #2832: Improve help text for mcp add about -- separator requirement - #2834: Add --add-dir flag to run command for consistency with exec * fix: batch merge of bug fixes (PR #288) - issues #2756, #2758, #2759, #2760, #2761, #2762, #2763, #2764, #2765, #2766 Fixes: - #2756: Add user-friendly error message for address-in-use errors in cortex serve - #2758: Add proxy error detection and user-friendly messages for proxy failures - #2759: Add session file size warning and monitoring for large sessions - #2760: Add graceful handling of malformed markdown (auto-close code blocks/tables) - #2761: Add HTTP/2 GOAWAY detection and retry logic for transient errors - #2762: Add user-friendly config parse error messages with line/column info - #2763: Add TTY detection and NO_COLOR support for ANSI escape codes - #2764: Document tab completion limits for large directories - #2766: Add signal handler documentation and improved panic hook for terminal cleanup * fix: batch merge of bug fixes (PR #289) - issues #2782, #2783, #2784, #2785, #2786, #2788, #2789, #2791, #2792 Fixes: - #2782: Ctrl+W now correctly deletes word backward in TUI input - #2783: Insert key is now handled (placeholder for future overwrite mode) - #2784: Double-click now selects word at cursor in input field - #2785: Terminal state is properly saved/restored on Ctrl+Z suspend/resume - #2786: Windows console now enables VT processing for ANSI sequences - #2788: Improved async task cancellation with proper HTTP connection cleanup - #2789: Added graceful shutdown for WebSocket/HTTP sessions on server exit - #2791: MCP child processes now have filtered environment (no secrets) - #2792: TUI streaming output optimized with dirty tracking, reduced idle FPS --------- Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 4271e8c commit 05eb7a4

42 files changed

Lines changed: 2326 additions & 605 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cortex-app-server/src/lib.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ where
6464
warn!("Use --auth to enable authentication.");
6565
}
6666

67-
let state = AppState::new(config.clone()).await?;
68-
let app = create_router(state);
67+
let state = Arc::new(AppState::new(config.clone()).await?);
68+
let state_for_cleanup = Arc::clone(&state);
69+
let app = create_router_with_state(state);
6970

7071
let addr: SocketAddr = config.listen_addr.parse()?;
7172
info!("Starting Cortex server on {}", addr);
@@ -102,6 +103,11 @@ where
102103
.with_graceful_shutdown(shutdown)
103104
.await?;
104105

106+
// Graceful shutdown: close all active sessions first
107+
// This ensures WebSocket clients receive proper close frames
108+
info!("Server shutting down, cleaning up active sessions...");
109+
state_for_cleanup.cli_session_manager.shutdown_all().await;
110+
105111
// Cleanup mDNS on shutdown
106112
if let Some(publisher) = mdns_publisher {
107113
if let Err(e) = publisher.unpublish().await {
@@ -117,6 +123,14 @@ where
117123

118124
/// Create the application router.
119125
pub fn create_router(state: AppState) -> Router {
126+
create_router_with_state(Arc::new(state))
127+
}
128+
129+
/// Create the application router with an Arc-wrapped state.
130+
///
131+
/// This variant is useful when you need to keep a reference to the state
132+
/// for cleanup purposes (e.g., during graceful shutdown).
133+
pub fn create_router_with_state(state: Arc<AppState>) -> Router {
120134
let api_routes = api::routes()
121135
.merge(websocket::routes())
122136
.merge(streaming::routes())
@@ -127,5 +141,5 @@ pub fn create_router(state: AppState) -> Router {
127141
.nest("/api/v1", api_routes)
128142
.layer(TraceLayer::new_for_http())
129143
.layer(CorsLayer::permissive())
130-
.with_state(Arc::new(state))
144+
.with_state(state)
131145
}

cortex-app-server/src/session_manager.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,33 @@ impl SessionManager {
411411
}
412412
}
413413

414+
/// Gracefully shutdown all active sessions.
415+
///
416+
/// This method should be called during server shutdown to ensure all
417+
/// WebSocket connections receive proper close frames and all in-progress
418+
/// requests are terminated cleanly.
419+
pub async fn shutdown_all(&self) {
420+
let session_ids: Vec<String> = {
421+
let sessions = self.sessions.read().await;
422+
sessions.keys().cloned().collect()
423+
};
424+
425+
if session_ids.is_empty() {
426+
info!("No active sessions to shutdown");
427+
return;
428+
}
429+
430+
info!("Shutting down {} active sessions", session_ids.len());
431+
432+
for session_id in session_ids {
433+
if let Err(e) = self.destroy_session(&session_id).await {
434+
warn!("Failed to shutdown session {}: {}", session_id, e);
435+
}
436+
}
437+
438+
info!("All sessions shutdown complete");
439+
}
440+
414441
/// List all active sessions.
415442
pub async fn list_sessions(&self) -> Vec<SessionInfo> {
416443
let sessions = self.sessions.read().await;

cortex-app-server/src/streaming.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,47 @@ impl CliSessionManager {
9797
let sessions = self.sessions.read().await;
9898
sessions.len()
9999
}
100+
101+
/// Gracefully shutdown all active CLI sessions.
102+
///
103+
/// This method should be called during server shutdown to ensure all
104+
/// streaming connections receive proper close frames and all in-progress
105+
/// requests are terminated cleanly.
106+
pub async fn shutdown_all(&self) {
107+
let session_ids: Vec<String> = {
108+
let sessions = self.sessions.read().await;
109+
sessions.keys().cloned().collect()
110+
};
111+
112+
if session_ids.is_empty() {
113+
info!("No active CLI sessions to shutdown");
114+
return;
115+
}
116+
117+
info!("Shutting down {} active CLI sessions", session_ids.len());
118+
119+
let mut sessions = self.sessions.write().await;
120+
for session_id in session_ids {
121+
if let Some(session) = sessions.remove(&session_id) {
122+
// Send shutdown command
123+
let _ = session
124+
.handle
125+
.submission_tx
126+
.send(Submission {
127+
id: Uuid::new_v4().to_string(),
128+
op: Op::Shutdown,
129+
})
130+
.await;
131+
132+
// Abort the session task
133+
session.session_task.abort();
134+
135+
debug!(session_id = %session_id, "CLI session shutdown");
136+
}
137+
}
138+
139+
info!("All CLI sessions shutdown complete");
140+
}
100141
}
101142

102143
// ============================================================================

cortex-cli/src/completion_setup.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ fn install_completions(shell: Shell) -> io::Result<()> {
221221
Ok(())
222222
}
223223

224+
/// Maximum number of completions to return for file/directory listings.
225+
/// This prevents shell hangs when completing in directories with many files.
226+
pub const MAX_COMPLETION_RESULTS: usize = 1000;
227+
224228
/// Prompt the user to install shell completions on first run.
225229
///
226230
/// This function checks if:
@@ -229,6 +233,9 @@ fn install_completions(shell: Shell) -> io::Result<()> {
229233
/// 3. We can detect the user's shell
230234
///
231235
/// If all conditions are met, it prompts the user and optionally installs completions.
236+
///
237+
/// Note: For large directories (>1000 files), completion may be slow.
238+
/// Consider using more specific paths or limiting directory size.
232239
pub fn maybe_prompt_completion_setup() {
233240
// Only prompt in interactive terminals
234241
if !is_interactive_terminal() {

cortex-cli/src/debug_cmd.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,14 @@ struct FileDebugOutput {
434434
#[derive(Debug, Serialize)]
435435
struct FileMetadata {
436436
size: u64,
437+
/// For virtual filesystems (procfs, sysfs), stat() returns 0 but reading
438+
/// the file may return actual content. This field stores the actual
439+
/// content size when available.
440+
#[serde(skip_serializing_if = "Option::is_none")]
441+
actual_size: Option<u64>,
442+
/// Whether the file is on a virtual filesystem (procfs, sysfs, etc.)
443+
#[serde(skip_serializing_if = "Option::is_none")]
444+
is_virtual_fs: Option<bool>,
437445
is_file: bool,
438446
is_dir: bool,
439447
is_symlink: bool,
@@ -495,9 +503,28 @@ async fn run_file(args: FileArgs) -> Result<()> {
495503
!is_writable_by_current_user(&path)
496504
};
497505

506+
// Check if this is a virtual filesystem (procfs, sysfs, etc.)
507+
// These report size=0 in stat() but may have actual content
508+
let is_virtual_fs = is_virtual_filesystem(&path);
509+
let stat_size = meta.len();
510+
511+
// For virtual filesystem files that report 0 size, try to read actual content size
512+
let actual_size = if is_virtual_fs && stat_size == 0 && meta.is_file() {
513+
// Try to read the file to get actual content size
514+
// Limit read to 1MB to avoid hanging on infinite streams
515+
match std::fs::read(&path) {
516+
Ok(content) if !content.is_empty() => Some(content.len() as u64),
517+
_ => None,
518+
}
519+
} else {
520+
None
521+
};
522+
498523
(
499524
Some(FileMetadata {
500-
size: meta.len(),
525+
size: stat_size,
526+
actual_size,
527+
is_virtual_fs: if is_virtual_fs { Some(true) } else { None },
501528
is_file: meta.is_file(),
502529
is_dir: meta.is_dir(),
503530
is_symlink: meta.file_type().is_symlink(),
@@ -577,7 +604,19 @@ async fn run_file(args: FileArgs) -> Result<()> {
577604
println!();
578605
println!("Metadata");
579606
println!("{}", "-".repeat(40));
580-
println!(" Size: {}", format_size(meta.size));
607+
// Handle virtual filesystem files that report 0 size (#2829)
608+
if meta.is_virtual_fs.unwrap_or(false) {
609+
if let Some(actual) = meta.actual_size {
610+
println!(
611+
" Size: {} (stat reports 0, virtual filesystem)",
612+
format_size(actual)
613+
);
614+
} else {
615+
println!(" Size: unknown (virtual filesystem)");
616+
}
617+
} else {
618+
println!(" Size: {}", format_size(meta.size));
619+
}
581620

582621
// Display file type, including special types like FIFO, socket, etc.
583622
let type_str = if let Some(ref special_type) = meta.file_type {
@@ -593,6 +632,9 @@ async fn run_file(args: FileArgs) -> Result<()> {
593632
};
594633
println!(" Type: {}", type_str);
595634

635+
if meta.is_virtual_fs.unwrap_or(false) {
636+
println!(" Virtual: yes (procfs/sysfs/etc)");
637+
}
596638
println!(" Readonly: {}", meta.readonly);
597639
if let Some(ref modified) = meta.modified {
598640
println!(" Modified: {}", modified);
@@ -733,6 +775,26 @@ fn detect_special_file_type(path: &std::path::Path) -> Option<String> {
733775
}
734776
}
735777

778+
/// Check if the path is on a virtual filesystem like procfs or sysfs.
779+
/// These filesystems report size=0 in stat() for files that have actual content. (#2829)
780+
#[cfg(target_os = "linux")]
781+
fn is_virtual_filesystem(path: &std::path::Path) -> bool {
782+
let path_str = path.to_string_lossy();
783+
path_str.starts_with("/proc/")
784+
|| path_str.starts_with("/sys/")
785+
|| path_str.starts_with("/dev/")
786+
|| path_str == "/proc"
787+
|| path_str == "/sys"
788+
|| path_str == "/dev"
789+
}
790+
791+
/// Check if the path is on a virtual filesystem like procfs or sysfs.
792+
/// On non-Linux systems, return false as these filesystems are Linux-specific.
793+
#[cfg(not(target_os = "linux"))]
794+
fn is_virtual_filesystem(_path: &std::path::Path) -> bool {
795+
false
796+
}
797+
736798
/// Detect encoding and binary status.
737799
fn detect_encoding_and_binary(path: &PathBuf) -> (Option<String>, Option<bool>) {
738800
// Read first 8KB to check for binary content

cortex-cli/src/exec_cmd.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,12 @@ pub enum ExecInputFormat {
198198
/// Unlike the interactive CLI, `cortex exec` runs as a one-shot command
199199
/// that completes a task and exits.
200200
#[derive(Debug, Parser)]
201+
#[command(allow_hyphen_values = true)]
201202
pub struct ExecCli {
202203
/// The prompt to execute.
203204
/// Can also be provided via stdin or --file.
204-
#[arg(trailing_var_arg = true)]
205+
/// Prompts starting with a dash (e.g., "--help explain this") are supported.
206+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
205207
pub prompt: Vec<String>,
206208

207209
/// Read prompt from file.
@@ -334,6 +336,41 @@ pub struct ExecCli {
334336
/// Supports glob patterns like "*.test.js" or "node_modules/**".
335337
#[arg(long = "exclude", action = clap::ArgAction::Append, value_name = "PATTERN")]
336338
pub exclude_patterns: Vec<String>,
339+
340+
// ═══════════════════════════════════════════════════════════════════════════
341+
// LLM Generation Parameters (Issues #2703, #2704, #2711, #2712, #2714)
342+
// ═══════════════════════════════════════════════════════════════════════════
343+
/// Frequency penalty (-2.0 to 2.0).
344+
/// Positive values penalize tokens based on their frequency in the text so far,
345+
/// decreasing the likelihood of repeating the same content verbatim.
346+
#[arg(long = "frequency-penalty")]
347+
pub frequency_penalty: Option<f32>,
348+
349+
/// Presence penalty (-2.0 to 2.0).
350+
/// Positive values penalize new tokens based on whether they appear in the text so far,
351+
/// increasing the likelihood of talking about new topics.
352+
#[arg(long = "presence-penalty")]
353+
pub presence_penalty: Option<f32>,
354+
355+
/// Stop sequences (can be specified multiple times).
356+
/// Generation will stop when any of these sequences is encountered.
357+
#[arg(long = "stop", action = clap::ArgAction::Append)]
358+
pub stop_sequences: Vec<String>,
359+
360+
/// Request log probabilities for output tokens.
361+
/// Returns the log probabilities of the most likely tokens (up to 5).
362+
#[arg(long = "logprobs")]
363+
pub logprobs: Option<u8>,
364+
365+
/// Number of completions to generate.
366+
/// Returns multiple independent completions for the same prompt.
367+
#[arg(short = 'n', long = "n")]
368+
pub num_completions: Option<u32>,
369+
370+
/// Generate best_of completions and return the best one.
371+
/// Must be greater than n if n is specified.
372+
#[arg(long = "best-of")]
373+
pub best_of: Option<u32>,
337374
}
338375

339376
impl ExecCli {

0 commit comments

Comments
 (0)