mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 17:10:44 -10:00
fix(runtime): Runtime API approvals never auto-deny; one [approval] timeout_seconds
The Runtime API approval path (what the desktop app and web use) denied a pending approval on its own after 300 seconds and interrupted the turn, because it borrowed `[tools] user_input_timeout_seconds` with a 300s default. CURRENT_DECISIONS §21: an approval waits for the person (sweep B3). - RuntimeThreadManager::approval_decision_timeout now reads the one approval clock the TUI card already uses, `[approval] timeout_seconds`: absent or 0 waits until the person answers or stops the turn. A set value still refuses fail-closed and is reported as a timeout (the existing approval.timeout event). - `[tools] user_input_timeout_seconds` governs request_user_input only. - docs/CONFIGURATION.md: both keys say what they govern. Tests: runtime_threads::tests::runtime_approvals_wait_indefinitely_unless_approval_timeout_is_set (new) passes in the 1099 passed / 0 failed focused run (runtime_threads:: 226+ tests, task_manager approval-suspension tests included). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01R9rJEoMuUSRWznU6QjkE7h
This commit is contained in:
1 parent
ab428ba1a7
commit
ed241c3a7f
3 files changed
+58
-21
No files matched your search
@@ -598,7 +598,6 @@ where
|
||||
}
|
||||
const RUNTIME_RESTART_REASON: &str = "Interrupted by process restart";
|
||||
const EMPTY_TURN_REASON: &str = "Turn completed without engine output";
|
||||
const APPROVAL_DECISION_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
const DYNAMIC_TOOL_RESULT_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -610,10 +609,11 @@ static TEST_DYNAMIC_TOOL_RESULT_TIMEOUT_MS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
impl RuntimeThreadManager {
|
||||
/// Wait for one external approval decision. `[tools]
|
||||
/// user_input_timeout_seconds` governs (#6003): absent uses the built-in
|
||||
/// default, an explicit 0 returns `None` and the decision waits
|
||||
/// indefinitely.
|
||||
/// Wait for one external approval decision. The one approval clock,
|
||||
/// `[approval] timeout_seconds`, governs here as it does for the TUI
|
||||
/// card: absent or `0` returns `None` and the decision waits until the
|
||||
/// person answers or stops the turn (CURRENT_DECISIONS §21). A GPUI or
|
||||
/// web approval is never denied on the user's behalf by default.
|
||||
pub(crate) fn approval_decision_timeout(&self) -> Option<Duration> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
@@ -622,11 +622,7 @@ impl RuntimeThreadManager {
|
||||
return Some(Duration::from_millis(ms));
|
||||
}
|
||||
}
|
||||
match self.read_config().user_input_timeout() {
|
||||
Some(wait) if wait.is_zero() => None,
|
||||
Some(wait) => Some(wait),
|
||||
None => Some(APPROVAL_DECISION_TIMEOUT),
|
||||
}
|
||||
self.read_config().approval_timeout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18235,3 +18235,43 @@ async fn canonical_sessions_root_is_resolved_off_the_ui_runtime_and_cached() ->
|
||||
assert_ne!(cached, sessions, "the fixture spells the root two ways");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// B3: Runtime API approvals (GPUI, web) follow the one approval clock,
|
||||
/// `[approval] timeout_seconds`. Unset, nothing denies on the user's behalf;
|
||||
/// `[tools] user_input_timeout_seconds` no longer bounds approvals.
|
||||
#[test]
|
||||
fn runtime_approvals_wait_indefinitely_unless_approval_timeout_is_set() -> Result<()> {
|
||||
let manager = test_manager(test_runtime_dir())?;
|
||||
assert_eq!(manager.approval_decision_timeout(), None);
|
||||
|
||||
let mut config = Config::default();
|
||||
config.tools = Some(crate::config::ToolsConfig {
|
||||
user_input_timeout_seconds: Some(300),
|
||||
..Default::default()
|
||||
});
|
||||
let manager = RuntimeThreadManager::open(
|
||||
config.clone(),
|
||||
PathBuf::from("."),
|
||||
test_manager_config(test_runtime_dir()),
|
||||
)?;
|
||||
assert_eq!(
|
||||
manager.approval_decision_timeout(),
|
||||
None,
|
||||
"the question clock does not bound approvals"
|
||||
);
|
||||
|
||||
config.approval = Some(crate::config::ApprovalConfig {
|
||||
timeout_seconds: Some(45),
|
||||
..Default::default()
|
||||
});
|
||||
let manager = RuntimeThreadManager::open(
|
||||
config,
|
||||
PathBuf::from("."),
|
||||
test_manager_config(test_runtime_dir()),
|
||||
)?;
|
||||
assert_eq!(
|
||||
manager.approval_decision_timeout(),
|
||||
Some(Duration::from_secs(45))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
+12
-11
@@ -2039,11 +2039,12 @@ reasoning contract, and all four membership ids omit generic sampling fields.
|
||||
default_selection = "allow_once"
|
||||
```
|
||||
- `[approval] timeout_seconds` (integer, optional): bound how long an
|
||||
interactive approval card may wait. When the window elapses the card
|
||||
resolves to **deny** — fail-closed, matching the external approval path —
|
||||
and the transcript records that the bound denied the call, not the
|
||||
operator. Omitted or `0` waits indefinitely, which stays the interactive
|
||||
default; values above 24h clamp with a warning (#6101).
|
||||
approval may wait — the TUI card and the Runtime API approvals the desktop
|
||||
app and web use alike. When the window elapses the call is refused
|
||||
(fail-closed) and recorded as a timeout, not as the operator's denial.
|
||||
Omitted or `0` waits indefinitely, which is the default everywhere: no
|
||||
approval is ever denied on your behalf unless you set this. Values above
|
||||
24h clamp with a warning (#6101).
|
||||
|
||||
```toml
|
||||
[approval]
|
||||
@@ -2928,10 +2929,10 @@ schema (`minItems` / `maxItems`), its model-visible description, and the
|
||||
payload validator. A rejected payload names the key to raise, so the model can
|
||||
either resize the batch or tell the user which setting to change.
|
||||
|
||||
### User-input / approval wait timeout
|
||||
### User-input wait timeout
|
||||
|
||||
Questions from `request_user_input` and approval decisions wait a bounded
|
||||
time and then cancel with a timeout (#6003). The default is 300 seconds.
|
||||
Questions from `request_user_input` wait a bounded time and then cancel with
|
||||
a timeout (#6003). The default is 300 seconds.
|
||||
Raise it when you step away or read carefully, or set `0` to wait forever
|
||||
(overnight automation, long human review). Headless `exec` runs have no
|
||||
responder, so `request_user_input` is withheld there by default:
|
||||
@@ -2942,9 +2943,9 @@ the model reports the tool absent and finishes instead of stalling.
|
||||
user_input_timeout_seconds = 300 # default 300; 0 disables the timeout; clamped to 86400 (24h)
|
||||
```
|
||||
|
||||
The one key governs both the interactive question wait and the Runtime
|
||||
approval-decision wait, and the wait is this table's only user-facing clock —
|
||||
wall-clock and stream protections elsewhere are unaffected.
|
||||
This key governs question waits only. Approvals have their own clock,
|
||||
`[approval] timeout_seconds`, which waits indefinitely unless you set it.
|
||||
Wall-clock and stream protections elsewhere are unaffected.
|
||||
|
||||
## Feature Flags
|
||||
|
||||
|
||||
Reference in new issue
Block a user