feat(worker): separate restored candidates, repair, and activation

This commit is contained in:
2026-10-22 22:58:57 +09:00
parent b5c105545c
commit 73a3e674c9
26 changed files with 6592 additions and 787 deletions
+33
View File
@@ -9,6 +9,7 @@ use crate::PromptCatalogSource;
use crate::controller::{
ShutdownReceiver, WorkerController, WorkerControllerTransport, WorkerHandle,
};
pub use crate::worker::{PreparedRestoredWorker, RestoreCandidateWorkdirCleanup};
use crate::worker::{Worker, WorkerError, WorkerFilesystemAuthority, WorkerWorkspaceContext};
use manifest::WorkerManifest;
@@ -168,6 +169,38 @@ where
Ok(PreparedWorker::new(worker, self.layout, self.transport))
}
/// Reconstruct a saved materialized Worker without allocation, attribution
/// repair, Feature installation, or controller exposure. The caller owns the
/// later controller layout/transport and must activate before starting it.
pub async fn prepare_restored_candidate(
self,
worker_name: &str,
) -> Result<PreparedRestoredWorker<St>, WorkerBootstrapError> {
let mut candidate =
Worker::prepare_restored_from_worker_metadata_with_context_and_model_client(
worker_name,
self.manifest,
self.store,
self.prompt_catalog,
self.workspace_context,
self.filesystem_authority,
self.model_client,
)
.await
.map_err(WorkerBootstrapError::Worker)?;
if let Some(workdir_session) = self.workdir_session {
candidate
.worker_mut()
.map_err(WorkerBootstrapError::Worker)?
.bind_single_workdir_session(Some(workdir_session));
}
candidate
.worker_mut()
.map_err(WorkerBootstrapError::Worker)?
.bind_subjektiv_host(self.subjektiv_host);
Ok(candidate)
}
pub async fn prepare_restored(
self,
worker_name: &str,
+29
View File
@@ -689,6 +689,35 @@ pub enum WorkerControllerTransport {
pub struct WorkerController;
impl WorkerController {
/// Explicit recovery of an unmaterialized Session head. This installs the
/// saved Feature composition needed to produce the seed, but creates no
/// Controller, input channel, socket, observer, or queue consumer. The caller
/// owns the same operation's Feature/session/allocation cleanup on every exit.
pub async fn recover_pending_head<C, St>(
worker: &mut Worker<C, St>,
runtime_base: &Path,
) -> std::io::Result<()>
where
C: LlmClient + Clone + 'static,
St: Store + WorkerMetadataStore + Clone + Send + Sync + 'static,
{
if worker.needs_initial_session_head_materialization() {
worker
.validate_subjektiv_body_publication()
.map_err(std::io::Error::other)?;
let registry = SpawnedWorkerRegistry::new_internal(
worker.manifest().worker.name.clone(),
worker.scope().clone(),
);
register_worker_tools(worker, runtime_base.to_path_buf(), registry, None, None).await?;
worker
.materialize_durable_session_head()
.await
.map_err(std::io::Error::other)?;
}
Ok(())
}
pub async fn spawn<C, St>(
worker: Worker<C, St>,
runtime_base: &Path,
+7 -4
View File
@@ -1066,7 +1066,8 @@ mod tests {
use tokio::net::UnixListener;
use crate::runtime::dir::RuntimeDir;
use crate::runtime::worker_allocation::test_util::RuntimeDirSandbox;
use crate::restore_test_environment::require_restore_test_environment;
#[derive(Clone)]
struct FailTargetPeerStore {
@@ -1110,11 +1111,13 @@ mod tests {
#[tokio::test(flavor = "current_thread")]
async fn state_backed_visibility_and_restore_planning() {
let root = TempDir::new().unwrap();
require_restore_test_environment();
let root = tempfile::tempdir_in(".").unwrap();
let store_dir = root.path().join("store");
let runtime_base = root.path().join("runtime");
// Bind, durable child metadata, and discovery probes all use this same
// short reference into the tree still owned by root. No cwd mutation.
let runtime_base = Path::new(root.path().file_name().unwrap()).join("runtime");
std::fs::create_dir_all(&runtime_base).unwrap();
let _runtime_sandbox = RuntimeDirSandbox::new(&runtime_base);
let store = FsWorkerStore::new(&store_dir).unwrap();
let session_id = new_session_id();
+3
View File
@@ -20,6 +20,9 @@ mod metadata_lifecycle;
pub mod model_client;
mod paste_artifact_tool;
pub mod prompt;
#[cfg(test)]
#[path = "../../../tests/support/restore_environment.rs"]
mod restore_test_environment;
pub mod runtime;
pub mod runtime_command;
pub mod segment_log_sink;
File diff suppressed because it is too large Load Diff
+28 -9
View File
@@ -1,3 +1,4 @@
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
@@ -150,6 +151,19 @@ impl LlmClient for MockClient {
// Helpers
// ---------------------------------------------------------------------------
#[path = "../../../tests/support/restore_environment.rs"]
mod restore_test_environment;
use restore_test_environment::require_restore_test_environment;
fn owned_relative_runtime_base() -> (tempfile::TempDir, PathBuf) {
require_restore_test_environment();
let temp = tempfile::tempdir_in(".").unwrap();
// The relative reference and the TempDir name the same owned tree, without
// putting the checkout prefix into sockaddr_un or changing process cwd.
let runtime_base = Path::new(temp.path().file_name().unwrap()).join("runtime");
(temp, runtime_base)
}
fn simple_text_events() -> Vec<LlmEvent> {
vec![
LlmEvent::text_block_start(0),
@@ -295,6 +309,7 @@ async fn make_worker_with_manifest_and_workspace_context(
manifest: WorkerManifest,
workspace_context: WorkerWorkspaceContext,
) -> (Worker<MockClient, TestStore>, std::path::PathBuf) {
require_restore_test_environment();
let store_tmp = tempfile::tempdir().unwrap();
let store = CombinedStore::new(
FsStore::new(store_tmp.path()).unwrap(),
@@ -320,18 +335,21 @@ async fn make_worker_with_manifest_and_workspace_context(
}
async fn spawn_controller(worker: Worker<MockClient, TestStore>) -> WorkerHandle {
let tmp = tempfile::tempdir().unwrap();
let runtime_base = tmp.path().to_owned();
std::mem::forget(tmp);
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
let (tmp, runtime_base) = owned_relative_runtime_base();
let (handle, shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
.await
.unwrap();
// Retain ownership until controller shutdown (or test-runtime teardown).
tokio::spawn(async move {
let _ = shutdown_rx.await;
drop(tmp);
});
handle
}
#[tokio::test]
async fn canonical_worker_handle_uploads_and_submits_attachment() {
let root = tempfile::tempdir().unwrap();
let (root, runtime_base) = owned_relative_runtime_base();
let manifest = WorkerManifest::from_toml(MANIFEST_TOML).unwrap();
let session = session_store::WorkerSessionStore::new(root.path().join("session")).unwrap();
let store = CombinedStore::new(
@@ -353,7 +371,7 @@ async fn canonical_worker_handle_uploads_and_submits_attachment() {
)
.await
.unwrap();
let (handle, shutdown_rx) = WorkerController::spawn(worker, &root.path().join("runtime"))
let (handle, shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
.await
.unwrap();
let mut rx = handle.subscribe();
@@ -1761,11 +1779,12 @@ permission = "write"
"#;
let client = MockClient::new(simple_text_events());
let worker = make_worker_with_pwd_and_manifest(client, manifest).await.0;
let tmp = tempfile::tempdir().unwrap();
let result = WorkerController::spawn(worker, tmp.path()).await;
let (_tmp, runtime_base) = owned_relative_runtime_base();
let result = WorkerController::spawn(worker, &runtime_base).await;
assert!(
result.is_ok(),
"feature exposure must not imply delegation authority"
"feature exposure must not imply delegation authority: {:?}",
result.as_ref().err()
);
}