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
26 changes: 25 additions & 1 deletion crates/path-cli/src/cmd_pathbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,34 @@ fn block_on<F: std::future::Future>(f: F) -> F::Output {
/// supplied. Progenitor doesn't expose a bearer-token setter, so we
/// pre-bake the header into the http client and hand it via
/// `Client::new_with_client`.
/// How long any pathbase request may take.
///
/// One flat value covered every call, and 30s is a fine ceiling for the
/// small ones — auth, listing repos, deleting a graph. It is the wrong
/// ceiling for an upload: a long agent session derives to several MB, and
/// the whole document goes up in one request. A real session hit this
/// against a *loopback* server, which is the case that should never time
/// out, and the error it produced — "request timed out after 30s — try
/// again, or shrink the upload" — invites you to shrink a conversation
/// that already happened.
///
/// So the default is generous and the value is settable. `PATH_HTTP_TIMEOUT_SECS`
/// covers every command, including the ones with no flag of their own — which
/// is what a launchd agent or a cron sweep can reach.
pub(crate) fn http_timeout() -> std::time::Duration {
const DEFAULT_SECS: u64 = 300;
let secs = std::env::var("PATH_HTTP_TIMEOUT_SECS")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|s| *s > 0)
.unwrap_or(DEFAULT_SECS);
std::time::Duration::from_secs(secs)
}

fn pathbase_client(base_url: &str, token: Option<&str>) -> Result<pathbase_client::Client> {
let mut builder = reqwest::Client::builder()
.user_agent(concat!("path-cli/", env!("CARGO_PKG_VERSION")))
.timeout(std::time::Duration::from_secs(30));
.timeout(http_timeout());
if let Some(t) = token {
let mut headers = reqwest::header::HeaderMap::new();
let mut auth = reqwest::header::HeaderValue::from_str(&format!("Bearer {t}"))
Expand Down
26 changes: 26 additions & 0 deletions crates/path-cli/src/cmd_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ pub struct ShareArgs {
/// Skip writing the cache; derive in-memory only
#[arg(long)]
pub no_cache: bool,

/// Seconds to allow the upload before giving up (default 300).
///
/// A long session derives to several MB and goes up in one request, so
/// the ceiling that suits every other call is the wrong one here. Sets
/// `PATH_HTTP_TIMEOUT_SECS` for this invocation; export that directly for
/// commands that have no flag, or for an agent that runs `share` for you.
#[arg(long, value_name = "SECS")]
pub timeout: Option<u64>,
}

/// One artifact surfaced by a provider — today always an agent session.
Expand Down Expand Up @@ -482,6 +491,15 @@ fn collect_cursor(
}

pub fn run(args: ShareArgs) -> Result<()> {
// Set before any client is built: `http_timeout` reads this, and every
// pathbase call in this process goes through one client constructor. Doing
// it here rather than threading a Duration keeps the eight call sites — all
// of them small requests that never needed a knob — untouched.
if let Some(secs) = args.timeout {
// SAFETY: single-threaded startup, before any client or task exists.
unsafe { std::env::set_var("PATH_HTTP_TIMEOUT_SECS", secs.to_string()) };
}

let harness = args.harness.map(|h| h.artifact_type());

if args.session.is_some() && harness.is_none() {
Expand Down Expand Up @@ -579,6 +597,9 @@ pub fn run(args: ShareArgs) -> Result<()> {
None
},
no_cache: args.no_cache,
// Already applied to the environment by `run`, so this is only carried
// for completeness — the picker path uploads through the same client.
timeout: args.timeout,
};
// Show the conversation title in the confirmation line; the session id
// is opaque and doesn't help the user verify they picked the right
Expand Down Expand Up @@ -1408,6 +1429,11 @@ mod tests {
session: None,
project: None,
no_cache: false,
// `run` applies this to the environment before any client exists,
// so it is never read on this path; None keeps these tests on the
// default timeout rather than asserting against a knob they do not
// exercise.
timeout: None,
}
}

Expand Down
Loading