agent_engine/session/budgets.rs
1//! Teardown budgets shared by every session host (values copied from
2//! `agent-tui/src/tui/signals.rs`; `signals.rs` re-exports these on day 2).
3//!
4//! - `SAVE_TIMEOUT_SECS` — session save + index end record (data safety first)
5//! - `HOOKS_TIMEOUT_SECS` — `on_session_end` hook emit (concurrent, fail-open)
6//! - `TEARDOWN_TIMEOUT_SECS` = their sum.
7
8pub const SAVE_TIMEOUT_SECS: u64 = 2;
9pub const HOOKS_TIMEOUT_SECS: u64 = 5;
10pub const TEARDOWN_TIMEOUT_SECS: u64 = SAVE_TIMEOUT_SECS + HOOKS_TIMEOUT_SECS;
11
12pub const SAVE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(SAVE_TIMEOUT_SECS);
13pub const HOOKS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(HOOKS_TIMEOUT_SECS);
14
15/// `SessionActor::cancel_turn`: how long a cancelled turn's stream is drained
16/// for the engine's cancel-path history, final Usage and any in-flight
17/// context-head checkpoint. The engine's provider/tool awaits are
18/// cancellation-first, so the tail normally arrives in milliseconds; on
19/// expiry the actor keeps its last adopted (valid) history. Also the grace a
20/// driver-revoked turn gets (`revoked_turn_deadline`).
21///
22/// One exception, deliberately unbounded: a context-head checkpoint the
23/// engine hands over while draining is persisted durably before its receipt
24/// is answered (a durability barrier; dropping it would latch the session).
25pub const CANCEL_DRAIN_TIMEOUT_MS: u64 = 1_000;
26pub const CANCEL_DRAIN_TIMEOUT: std::time::Duration =
27 std::time::Duration::from_millis(CANCEL_DRAIN_TIMEOUT_MS);
28/// Whole-second ceiling of the drain, for the budget sums below/elsewhere.
29pub const CANCEL_DRAIN_TIMEOUT_SECS: u64 = CANCEL_DRAIN_TIMEOUT_MS.div_ceil(1000);
30/// Worst case of `cancel_turn`: the drain, then the bounded wait for the
31/// interrupted turn's save before it announces `Idle` (`announce_idle`).
32pub const CANCEL_TURN_TIMEOUT_SECS: u64 = CANCEL_DRAIN_TIMEOUT_SECS + SAVE_TIMEOUT_SECS;
33
34/// `SessionActor::checkpoint` for a daemon reload: how long the workers it
35/// stopped get to finish stopping (cancel → the runtime's cancel-path
36/// history → the finalizer's resume archive) before the exec destroys them.
37/// Cancellation is cooperative and normally takes milliseconds; a worker
38/// blocked in a tool that ignores cancellation is abandoned with whatever
39/// archive it already had.
40pub const WORKER_RELOAD_STOP_TIMEOUT_SECS: u64 = 10;
41pub const WORKER_RELOAD_STOP_TIMEOUT: std::time::Duration =
42 std::time::Duration::from_secs(WORKER_RELOAD_STOP_TIMEOUT_SECS);
43
44/// Longest a reload checkpoint fences its session (no queue-wake turns,
45/// keep-warm pings, driver ticks or parking) waiting for the `execv` or for
46/// the daemon to report it failed (`ReloadAborted`). Both normally come
47/// within seconds; this only bounds a reload that never reports back (a
48/// daemon bug: clients cannot send a reload checkpoint).
49pub const RELOAD_FENCE_TIMEOUT_SECS: u64 = 120;
50pub const RELOAD_FENCE_TIMEOUT: std::time::Duration =
51 std::time::Duration::from_secs(RELOAD_FENCE_TIMEOUT_SECS);
52
53// ── daemon reload steps after the checkpoints (`daemon::reload`) ──
54/// Per-session `Checkpoint{Reload}` wait: `cancel_turn` (the drain + its
55/// bounded save) + stopping the session's workers + the checkpoint's own
56/// bounded save + 1 s margin. Sessions checkpoint concurrently.
57pub const RELOAD_CHECKPOINT_BUDGET_SECS: u64 =
58 CANCEL_TURN_TIMEOUT_SECS + WORKER_RELOAD_STOP_TIMEOUT_SECS + SAVE_TIMEOUT_SECS + 1;
59/// Other connections' `Bye{Reloading}` writers, after the announce.
60pub const RELOAD_ANNOUNCE_WAIT_SECS: u64 = 1;
61/// The requesting connection flushes its own `Bye` before the exec.
62pub const RELOAD_BYE_FLUSH_SECS: u64 = 1;
63/// Extension sidecars shut down for the exec.
64pub const RELOAD_EXT_SHUTDOWN_SECS: u64 = 5;
65/// A failed exec re-discovers the extensions before the sessions resume.
66pub const RELOAD_EXT_REDISCOVERY_SECS: u64 = 10;
67/// Delivering `ReloadAborted` to a session whose command queue is full.
68pub const RELOAD_ABORT_SEND_SECS: u64 = 5;
69/// Longest a session stays fenced before its process is replaced or it is
70/// told the exec failed: the slowest other session's checkpoint, the
71/// announce, the flush, the sidecar shutdown, then (failed exec) the
72/// re-discovery and the abort delivery.
73pub const RELOAD_FENCE_WORST_CASE_SECS: u64 = RELOAD_CHECKPOINT_BUDGET_SECS
74 + RELOAD_ANNOUNCE_WAIT_SECS
75 + RELOAD_BYE_FLUSH_SECS
76 + RELOAD_EXT_SHUTDOWN_SECS
77 + RELOAD_EXT_REDISCOVERY_SECS
78 + RELOAD_ABORT_SEND_SECS;
79// The backstop must never lift a fence while the exec (or its abort) is
80// still coming: a turn run then could save history holding a completion
81// the next image re-posts, waking the parent twice.
82const _: () = assert!(RELOAD_FENCE_TIMEOUT_SECS > 2 * RELOAD_FENCE_WORST_CASE_SECS);
83
84/// Session end's bounded observability flush (`finish` STEP 3).
85pub const OBSERVABILITY_FLUSH_TIMEOUT_SECS: u64 =
86 crate::runtime::telemetry::DEFAULT_SHUTDOWN_FLUSH_TIMEOUT.as_secs();
87
88/// Worst case of `SessionActor::finish`: cancel a running turn, one bounded
89/// save, the `on_session_end` hooks, the observability flush.
90pub const SESSION_END_TIMEOUT_SECS: u64 = CANCEL_TURN_TIMEOUT_SECS
91 + SAVE_TIMEOUT_SECS
92 + HOOKS_TIMEOUT_SECS
93 + OBSERVABILITY_FLUSH_TIMEOUT_SECS;
94
95/// `SessionActor::create` bound on `EngineHost::extensions_ready()`: the
96/// loader guard should make this unreachable; it exists so a session can
97/// never hang on a loader that never reports (warns, then proceeds).
98pub const EXTENSIONS_READY_TIMEOUT_SECS: u64 = 30;
99pub const EXTENSIONS_READY_TIMEOUT: std::time::Duration =
100 std::time::Duration::from_secs(EXTENSIONS_READY_TIMEOUT_SECS);
101
102/// `SessionActor::unpark` bound (B3): journal load + runtime rebuild.
103/// Transports wait `ATTACH_TIMEOUT_PARKED` (25 s) for a parked attach.
104pub const UNPARK_TIMEOUT_SECS: u64 = 20;
105pub const UNPARK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(UNPARK_TIMEOUT_SECS);