Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.199"
version = "0.1.200"
edition = "2024"
rust-version = "1.88"
license = "Apache-2.0"
Expand Down
23 changes: 17 additions & 6 deletions crates/cli-sub-agent/src/session_cmds_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ fn read_daemon_pid(session_dir: &std::path::Path) -> Option<u32> {
///
/// Exits 0 when result.toml appears (streams stdout.log), exits 124 on timeout,
/// exits 1 if the daemon process died without producing a result.
/// Hardcoded wait timeout in seconds.
const WAIT_TIMEOUT_SECS: u64 = 250;

pub(crate) fn handle_session_wait(session: String, cd: Option<String>) -> Result<i32> {
pub(crate) fn handle_session_wait(
session: String,
cd: Option<String>,
wait_timeout_secs: u64,
) -> Result<i32> {
let project_root = crate::pipeline::determine_project_root(cd.as_deref())?;
let resolved = resolve_session_prefix_with_fallback(&project_root, &session)?;
let session_dir = get_session_dir(&project_root, &resolved.session_id)?;
Expand Down Expand Up @@ -84,10 +85,20 @@ pub(crate) fn handle_session_wait(session: String, cd: Option<String>) -> Result
return Ok(1);
}

if start.elapsed().as_secs() >= WAIT_TIMEOUT_SECS {
let elapsed = start.elapsed().as_secs();
if elapsed >= wait_timeout_secs {
eprintln!(
"Timeout: session {} did not complete within {}s",
resolved.session_id, WAIT_TIMEOUT_SECS
resolved.session_id, wait_timeout_secs,
);
// Emit structured retry hint for orchestrators / agents.
let cd_arg = cd
.as_ref()
.map(|path| format!(" --cd '{}'", path))
.unwrap_or_default();
eprintln!(
"<!-- CSA:SESSION_WAIT_TIMEOUT session={} elapsed={}s cmd=\"csa session wait --session {}{}\" -->",
resolved.session_id, elapsed, resolved.session_id, cd_arg,
);
Comment on lines +99 to 102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The structured retry hint does not include the --cd argument if it was provided to the original command. If an orchestrator or agent relies on this hint to re-invoke the command from a different working directory, the retry might fail because it won't be able to locate the session or project root.

Suggested change
eprintln!(
"<!-- CSA:SESSION_WAIT_TIMEOUT session={} elapsed={}s cmd=\"csa session wait --session {}\" -->",
resolved.session_id, elapsed, resolved.session_id,
);
let cd_arg = cd.as_ref().map(|path| format!(" --cd \"{}\"", path)).unwrap_or_default();
eprintln!(
"<!-- CSA:SESSION_WAIT_TIMEOUT session={} elapsed={}s cmd=\"csa session wait --session {}{}\" -->",
resolved.session_id, elapsed, resolved.session_id, cd_arg,
);

Comment on lines +95 to 102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The structured retry hint contains nested double quotes in the cmd attribute, which will break parsing for any standard XML/HTML attribute parser. Specifically, the cmd attribute value is delimited by double quotes, and cd_arg also introduces double quotes around the path.

Additionally, using single quotes for the path in the command hint is generally safer and avoids conflict with the outer double quotes of the attribute.

Suggested change
let cd_arg = cd
.as_ref()
.map(|path| format!(" --cd \"{}\"", path))
.unwrap_or_default();
eprintln!(
"<!-- CSA:SESSION_WAIT_TIMEOUT session={} elapsed={}s cmd=\"csa session wait --session {}{}\" -->",
resolved.session_id, elapsed, resolved.session_id, cd_arg,
);
// Emit structured retry hint for orchestrators / agents.
let cd_arg = cd
.as_ref()
.map(|path| format!(" --cd '{}'", path))
.unwrap_or_default();
eprintln!(
"<!-- CSA:SESSION_WAIT_TIMEOUT session={} elapsed={}s cmd=\"csa session wait --session {}{}\" -->",
resolved.session_id, elapsed, resolved.session_id, cd_arg,
);

Comment on lines +95 to 102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The structured retry hint does not escape the cd path or the overall command string. If the path contains single quotes, the suggested shell command will be broken. Furthermore, if the path contains double quotes or other XML-sensitive characters, it will break the XML-like attribute structure of the directive.

It is recommended to escape single quotes for the shell command and XML-sensitive characters for the attribute value, and to quote all attribute values for better machine readability.

            let cd_arg = cd
                .as_ref()
                .map(|path| format!(" --cd '{}'", path.replace('\'', "'\\''")))
                .unwrap_or_default();
            let cmd = format!("csa session wait --session {}{}", resolved.session_id, cd_arg);
            eprintln!(
                "<!-- CSA:SESSION_WAIT_TIMEOUT session=\"{}\" elapsed=\"{}s\" cmd=\"{}\" -->",
                resolved.session_id,
                elapsed,
                cmd.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
            );

return Ok(124);
}
Expand Down
20 changes: 19 additions & 1 deletion crates/cli-sub-agent/src/session_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use anyhow::Result;

use crate::cli::SessionCommands;
use crate::session_cmds;
use csa_config::DEFAULT_DAEMON_WAIT_SECS;
use csa_core::types::OutputFormat;

pub(crate) fn dispatch(cmd: SessionCommands, output_format: OutputFormat) -> Result<()> {
Expand Down Expand Up @@ -90,7 +91,8 @@ pub(crate) fn dispatch(cmd: SessionCommands, output_format: OutputFormat) -> Res
session_cmds::handle_session_tool_output(session, index, list, cd)?;
}
SessionCommands::Wait { session, cd } => {
let exit_code = session_cmds::handle_session_wait(session, cd)?;
let wait_timeout = resolve_daemon_wait_timeout(cd.as_deref());
let exit_code = session_cmds::handle_session_wait(session, cd, wait_timeout)?;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
std::process::exit(exit_code);
Expand All @@ -111,3 +113,19 @@ pub(crate) fn dispatch(cmd: SessionCommands, output_format: OutputFormat) -> Res
}
Ok(())
}

/// Resolve daemon wait timeout from project/global config, falling back to the
/// compile-time default.
fn resolve_daemon_wait_timeout(cd: Option<&str>) -> u64 {
let project_root = crate::pipeline::determine_project_root(cd).ok();
if let Some(ref root) = project_root {
match csa_config::ProjectConfig::load(root) {
Ok(Some(config)) => return config.session.daemon_wait_seconds,
Ok(None) => {} // No project config file — use default.
Err(e) => {
tracing::warn!("Failed to load project config for daemon_wait_seconds: {e}")
}
}
}
DEFAULT_DAEMON_WAIT_SECS
}
4 changes: 3 additions & 1 deletion crates/csa-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ fn default_recursion_depth() -> u32 {
5
}

pub use super::config_session::{ExecutionConfig, HooksSection, SessionConfig, VcsConfig};
pub use super::config_session::{
DEFAULT_DAEMON_WAIT_SECS, ExecutionConfig, HooksSection, SessionConfig, VcsConfig,
};
pub use super::config_tool::{ToolConfig, ToolFilesystemSandboxConfig, ToolRestrictions};

impl ProjectConfig {
Expand Down
16 changes: 16 additions & 0 deletions crates/csa-config/src/config_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ pub struct SessionConfig {
/// Only effective when `tool_output_compression` is enabled.
#[serde(default = "default_tool_output_threshold_bytes")]
pub tool_output_threshold_bytes: u64,
/// Timeout (seconds) for `csa session wait` polling loop.
///
/// The default of 250s is intentional: it lets the daemon's KV cache stay
/// warm while periodically returning control to the calling orchestrator.
/// The caller is expected to re-invoke `csa session wait` in a loop.
#[serde(default = "default_daemon_wait_seconds")]
pub daemon_wait_seconds: u64,
}

fn default_seed_max_age_secs() -> u64 {
Expand All @@ -65,6 +72,13 @@ fn default_tool_output_threshold_bytes() -> u64 {
8192
}

/// Default daemon wait timeout: 250s for KV cache warmth.
pub const DEFAULT_DAEMON_WAIT_SECS: u64 = 250;

fn default_daemon_wait_seconds() -> u64 {
DEFAULT_DAEMON_WAIT_SECS
}
Comment on lines +75 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default timeout value of 250 is hardcoded in multiple places across different crates. It is better to define it as a public constant in this crate so that it can be reused by other crates (like cli-sub-agent), avoiding magic numbers and ensuring consistency.

Suggested change
/// Default daemon wait timeout: 250s for KV cache warmth.
fn default_daemon_wait_seconds() -> u64 {
250
}
/// Default daemon wait timeout: 250s for KV cache warmth.
pub const DEFAULT_DAEMON_WAIT_SECS: u64 = 250;
fn default_daemon_wait_seconds() -> u64 {
DEFAULT_DAEMON_WAIT_SECS
}


const DEFAULT_SPOOL_MAX_MB: u32 = 32;
const DEFAULT_SPOOL_KEEP_ROTATED: bool = true;

Expand All @@ -82,6 +96,7 @@ impl Default for SessionConfig {
spool_keep_rotated: None,
tool_output_compression: false,
tool_output_threshold_bytes: default_tool_output_threshold_bytes(),
daemon_wait_seconds: default_daemon_wait_seconds(),
}
}
}
Expand All @@ -99,6 +114,7 @@ impl SessionConfig {
&& self.spool_keep_rotated.is_none()
&& !self.tool_output_compression
&& self.tool_output_threshold_bytes == default_tool_output_threshold_bytes()
&& self.daemon_wait_seconds == default_daemon_wait_seconds()
}

pub fn resolved_spool_max_mb(&self) -> u32 {
Expand Down
6 changes: 3 additions & 3 deletions crates/csa-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ pub mod weave_lock;

pub use acp::AcpConfig;
pub use config::{
EnforcementMode, ExecutionConfig, HooksSection, ProjectConfig, ProjectMeta, SessionConfig,
TierConfig, TierStrategy, ToolConfig, ToolFilesystemSandboxConfig, ToolResourceProfile,
ToolRestrictions,
DEFAULT_DAEMON_WAIT_SECS, EnforcementMode, ExecutionConfig, HooksSection, ProjectConfig,
ProjectMeta, SessionConfig, TierConfig, TierStrategy, ToolConfig, ToolFilesystemSandboxConfig,
ToolResourceProfile, ToolRestrictions,
};
pub use config_filesystem_sandbox::FilesystemSandboxConfig;
pub use config_resources::ResourcesConfig;
Expand Down
Loading