fix(startup): thread-exhaustion resilience — cap workers, never panic

Port of upstream grok-build 0.2.113/0.2.114 fixes, extended to Kigi's
startup order.

- kigi-tty-utils runtime module: worker-cap policy (8 max) — tokio's
  per-core default pins 100+ thread slots on many-core shared hosts
  against RLIMIT_NPROC / pids.max, and later spawns die with EAGAIN.
- kigi-bin builds the main runtime with capped workers and reports an
  out-of-threads host with a diagnostic + exit(1) instead of a panic.
- The pre-runtime optimization threads (HTTP warmup, early model
  prefetch) spawn recoverably and degrade — their panic would have
  preempted the runtime's own diagnostic.
- The per-session actor thread and its runtime report spawn/build
  failure through the existing acp::Error / AgentBuildError boundaries
  (new RuntimeBuild variant) instead of panicking the agent host.
- fsnotify's watcher thread spawn maps into FsNotifyError::WatcherStart
  so callers degrade instead of crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
This commit is contained in:
ZacharyZhang-NYandClaude Fable 5 committed 2026-08-27 11:28:17 -04:00
1 parent bf93091416
commit 97de661554
8 files changed
+144 -17

No files matched your search

+17 -5
View File
@@ -1938,7 +1938,7 @@ fn resolve_prefetch_env(kimi_code_config: Option<KimiCodeConfig>) -> Option<Pref
/// credentials from disk.
pub fn start_early_prefetch_with_auth(auth: Option<KimiAuth>) -> Option<EarlyPrefetchHandle> {
let env = resolve_prefetch_env_with_auth(auth)?;
Some(spawn_prefetch_thread(env))
spawn_prefetch_thread(env)
}
/// Start the model-catalog prefetch on a background thread.
@@ -1949,11 +1949,16 @@ pub fn start_early_prefetch(
kimi_code_config: Option<KimiCodeConfig>,
) -> Option<EarlyPrefetchHandle> {
let env = resolve_prefetch_env(kimi_code_config)?;
Some(spawn_prefetch_thread(env))
spawn_prefetch_thread(env)
}
fn spawn_prefetch_thread(env: PrefetchEnv) -> EarlyPrefetchHandle {
std::thread::spawn(move || {
/// `None` when the OS refuses a thread: the prefetch is an optimization and
/// runs before the runtime builds, so a spawn panic here would preempt the
/// runtime's own out-of-threads diagnostic.
fn spawn_prefetch_thread(env: PrefetchEnv) -> Option<EarlyPrefetchHandle> {
let spawned = std::thread::Builder::new()
.name("early-prefetch".into())
.spawn(move || {
let mut timer = crate::instrumentation_timer!("startup.early_prefetch");
let proxy_endpoint = env.endpoints.proxy_url();
timer.with_field("endpoint", proxy_endpoint.as_str());
@@ -1993,7 +1998,14 @@ fn spawn_prefetch_thread(env: PrefetchEnv) -> EarlyPrefetchHandle {
}
EarlyPrefetchResult { models }
})
});
match spawned {
Ok(handle) => Some(handle),
Err(e) => {
tracing::error!(error = %e, "early prefetch thread spawn failed; skipping prefetch");
None
}
}
}
/// Map a model id (catalog key or routing slug) to its catalog key.