feat(tui): expose stream chunk timeout config#2507
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new stream_chunk_timeout_secs configuration option to manage SSE stream stall timeouts, including support for persisting, setting, and displaying this value in the TUI. Feedback on these changes highlights critical safety and performance concerns: mutating the environment dynamically with std::env::set_var in a multi-threaded context is unsafe and should be replaced by passing the timeout directly; casting u64 to i64 using as risks silent overflow and should use safe conversion; and loading the configuration synchronously on the UI thread can cause lag, so the configuration should be loaded once and reused.
| unsafe { | ||
| std::env::set_var(STREAM_CHUNK_TIMEOUT_ENV, resolved.to_string()); | ||
| } |
There was a problem hiding this comment.
Calling std::env::set_var in a multi-threaded environment is inherently unsafe and can lead to undefined behavior or crashes (such as segmentation faults) if other threads are concurrently reading the environment (e.g., via std::env::var or standard library/C library calls).
Since this is a TUI application running a background engine on a multi-threaded Tokio runtime, mutating the environment at runtime (especially when the user dynamically changes settings or when spawning the engine) is highly risky.
Recommendation:
Instead of passing the timeout configuration via an environment variable, refactor the engine/SSE client to accept the timeout value directly from the Config or EngineConfig struct. This avoids the need for unsafe environment mutation entirely.
| unsafe { | ||
| std::env::set_var( | ||
| STREAM_CHUNK_TIMEOUT_ENV, | ||
| self.stream_chunk_timeout_secs().to_string(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Calling std::env::set_var in a multi-threaded environment is inherently unsafe and can lead to undefined behavior or crashes (such as segmentation faults) if other threads are concurrently reading the environment (e.g., via std::env::var or standard library/C library calls).
Since this is a TUI application running a background engine on a multi-threaded Tokio runtime, mutating the environment at runtime (especially when the user dynamically changes settings or when spawning the engine) is highly risky.
Recommendation:
Instead of passing the timeout configuration via an environment variable, refactor the engine/SSE client to accept the timeout value directly from the Config or EngineConfig struct. This avoids the need for unsafe environment mutation entirely.
| let tui_table = tui_entry | ||
| .as_table_mut() | ||
| .context("config.toml [tui] must be a table")?; | ||
| tui_table.insert(key.to_string(), toml::Value::Integer(value as i64)); |
There was a problem hiding this comment.
Casting value from u64 to i64 using as i64 can silently overflow and result in a negative integer if value exceeds i64::MAX. Since persist_tui_integer_key is a public helper function, it could be reused in the future for other configuration keys where the value might exceed i64::MAX.
Consider using try_into() to safely convert the value, or adding a check to ensure it fits within i64 bounds.
let val_i64 = i64::try_from(value).context("value exceeds maximum supported integer size")?;
tui_table.insert(key.to_string(), toml::Value::Integer(val_i64));| value: Config::load(app.config_path.clone(), app.config_profile.as_deref()) | ||
| .map(|config| config.stream_chunk_timeout_secs().to_string()) | ||
| .unwrap_or_else(|_| "(unavailable)".to_string()), |
There was a problem hiding this comment.
Calling Config::load synchronously inside the initialization of a single ConfigRow performs disk I/O on the UI thread. If multiple rows in the ConfigView load the configuration independently, this can cause noticeable lag or stuttering in the TUI.
Recommendation:
Load the Config once at the beginning of ConfigView::new_for_app (or the relevant view constructor) and reuse that loaded instance across all rows that need to read from the saved configuration.
|
Thanks @cyq1017. I reviewed this during the v0.8.50 triage pass and am not harvesting it yet. The user-facing knob is plausible, but this needs one more design pass before it is release-safe. Concrete next steps:
Once those are addressed, this could be a good follow-up for #2365 / stalled stream tuning. For v0.8.50 I am keeping it out because #2487 is already broad and we should not add a new timeout-control surface with these unresolved runtime risks. |
|
Thanks for the quick follow-up, @cyq1017. I rechecked the new I am still keeping it out of #2504 for now because a couple of release-polish items remain:
Once those are cleaned up and Windows CI is green, this looks like a solid follow-up for #2365. |
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Problem
Change
[tui].stream_chunk_timeout_secswith the existing 300 second default./config stream_chunk_timeout_secs, persisted config, and the config view.0meaning the default.Verification
cargo test -p codewhale-tui stream_chunk --all-features --locked -- --nocapturecargo check -p codewhale-tui --all-features --lockedcargo fmt --all -- --checkgit diff --check origin/main..HEADCloses #2365
Greptile Summary
This PR exposes the existing SSE stream-chunk idle timeout as a user-configurable setting (
[tui].stream_chunk_timeout_secs, default 300 s, clamped to 1–3600 s). The timeout is wired from config →App→EngineConfig→ turn loop at startup, and can be updated live via/config stream_chunk_timeout_secsor the config view without a restart.stream_chunk_timeout_secs()env-reader instreaming.rsis removed; the value now travels throughEngineConfig.stream_chunk_timeoutand is mutable via the newOp::SetStreamChunkTimeoutop.ConfigSection::Networkvariant is added to the config view so the setting lands in a logical group rather than under Display.show_single_settingpath for this key reloads config from disk instead of readingapp.stream_chunk_timeout_secs, causing/config stream_chunk_timeout_secs(query) to report a stale value after a session-only change.Confidence Score: 4/5
The core timeout wiring is correct and the new setting takes effect immediately for subsequent turns. One path is wrong: querying the setting after a session-only change shows the on-disk value, not the live value.
All the plumbing — EngineConfig, Op dispatch, App state initialization, config view, and persist path — is correct. The one defect is in show_single_setting: it reloads Config from disk instead of reading app.stream_chunk_timeout_secs, so the query output contradicts the session-only confirmation the user just saw.
crates/tui/src/commands/config.rs — the show_single_setting branch for stream_chunk_timeout_secs reads from disk instead of live app state
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant ConfigCmd as /config command participant App participant UI as ui.rs participant Engine participant TurnLoop as turn_loop User->>ConfigCmd: /config stream_chunk_timeout_secs 90 ConfigCmd->>App: "app.stream_chunk_timeout_secs = 90" ConfigCmd-->>UI: AppAction::UpdateStreamChunkTimeout(90) UI->>Engine: "Op::SetStreamChunkTimeout { timeout_secs: 90 }" Engine->>Engine: "config.stream_chunk_timeout = Duration::from_secs(90)" Note over TurnLoop: Next turn uses updated value TurnLoop->>Engine: "stream_chunk_timeout_budget(&self.config)" Engine-->>TurnLoop: (90, Duration::from_secs(90)) User->>ConfigCmd: /config stream_chunk_timeout_secs (query) Note over ConfigCmd: reads Config::load() from disk,<br/>not app.stream_chunk_timeout_secs,<br/>shows 300 instead of 90Reviews (4): Last reviewed commit: "fix(tui): keep config sections grouped" | Re-trigger Greptile