Persist loaded threads before managed daemon shutdown (#44283)

## Why

Loaded threads, including idle threads whose rollout files are still deferred, need to survive a managed daemon restart. Shutdown must also remain forceable when rollout I/O is blocked.

## What changed

- Add a hidden `--managed-daemon` option for app-server Unix socket transports. After active turns and admitted requests drain, save loaded persistent root threads before exiting.
- Skip ephemeral threads, non-root agent threads, and threads pending unload. Log persistence failures and continue saving other threads.
- Apply shutdown admission checks to thread and turn settings updates, thread deletion, and archiving.
- Keep force signals and daemon shutdown requests responsive during persistence. Return `AppServerExit::Forced` so executables can exit without waiting for runtime teardown.

## Testing

Add integration coverage for resuming active and idle threads after restart, forcing shutdown during active work, and forcing shutdown with a blocked rollout writer. Extend shutdown rejection coverage to deletion and settings updates.

GitOrigin-RevId: 4344e97d39f9f80c5d84c41c17300fd6aa99b4cc
This commit is contained in:
Eric Traut
2026-09-09 19:22:22 +00:00
committed by copyberry
parent fcd90d8f07
commit c1840dc55e
12 changed files with 465 additions and 16 deletions
+9 -1
View File
@@ -759,6 +759,10 @@ impl MessageProcessor {
self.thread_processor.thread_created_receiver()
}
pub(crate) async fn persist_daemon_threads(&self) {
self.thread_processor.persist_daemon_threads().await
}
pub(crate) async fn send_initialize_notifications_to_connection(
&self,
connection_id: ConnectionId,
@@ -951,7 +955,11 @@ impl MessageProcessor {
| ClientRequest::ThreadFork { .. }
| ClientRequest::ThreadResume { .. }
| ClientRequest::ThreadRollback { .. }
| ClientRequest::ThreadRevert { .. } => (Some(self.turn_admission.admit()?), false),
| ClientRequest::ThreadRevert { .. }
| ClientRequest::ThreadSettingsUpdate { .. }
| ClientRequest::TurnSettingsUpdate { .. }
| ClientRequest::ThreadDelete { .. }
| ClientRequest::ThreadArchive { .. } => (Some(self.turn_admission.admit()?), false),
ClientRequest::TurnStart { .. }
| ClientRequest::TurnSteer { .. }
| ClientRequest::ReviewStart { .. }