fix(desktop): integrate lifecycle with captured launch admission

Use producer preflight authority and shared captured Start/Restart admission. Capture the existing explicit Start Stop fence and runtime generation before await; keep probes and Restart without resume authority and preserve Failed after destructive Stop. Broker remains provisioning-unavailable. Native compilation remains gated on CI-only draft authorization; local formatting and integrated frontend checks pass.

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-07 12:29:31 -04:00
parent a35d9497cd
commit a7d1357c84
3 changed files with 102 additions and 35 deletions

View File

@@ -339,7 +339,7 @@ fn execute(
conn: &rusqlite::Connection,
owner: &str,
request: &Request,
prepared: Result<&managed_agents::runtime_configurations::PreparedLaunch, &Outcome>,
prepared: Result<&configurations::CapturedLaunch, &Outcome>,
) -> Result<Outcome, String> {
let target = &request.target;
if request.configuration.is_none() {
@@ -375,37 +375,40 @@ fn execute(
community: &target.community,
agent: &target.agent,
},
plan.record(),
plan.plan.record(),
) {
Ok(b) => b,
Err(outcome) => return Ok(outcome),
};
if request.action == Action::Restart
&& managed_agents::stop_pair_locked(
target.agent.clone(),
target.community.clone(),
app.clone(),
)
.is_err()
{
return Ok(Outcome::Failed);
}
// Existing transition lock spans final intent check, ordinary launch and registration.
// Shared admission validates the captured Stop fence and generation BEFORE
// destructive Restart. The transition lock spans that Stop, launch and receipt.
if placement::blocked(conn, &target.agent, &target.desktop)? {
return Ok(Outcome::Unknown);
}
match managed_agents::start_pair_prepared_locked(
match managed_agents::start_pair_captured_locked(
target.agent.clone(),
target.community.clone(),
true,
None,
true,
plan.resume.as_ref(),
Some(&broker),
Some(plan),
&plan.plan,
false, // Explicit wire reference, not the destination's next selection.
request.action == Action::Restart,
Some(&plan.generation),
app.clone(),
) {
Ok(_) => Ok(Outcome::Running),
Err(_) => Ok(Outcome::Failed),
Ok(status)
if status.running_configuration == request.configuration
&& !matches!(
status.lifecycle,
managed_agents::ManagedAgentRuntimeLifecycle::Failed
| managed_agents::ManagedAgentRuntimeLifecycle::Stopped
) =>
{
Ok(Outcome::Running)
}
Ok(_) | Err(_) => Ok(Outcome::Failed),
}
}

View File

@@ -3,6 +3,13 @@ use super::*;
use buzz_core_pkg::desktop_lifecycle::{CatalogPage, Observation, RuntimeConfigurationSummary};
use managed_agents::runtime_configurations::{self as configurations, PreparedLaunch};
/// The shared launch plan and its pre-await lifecycle fences; never wire data.
pub(super) struct CapturedLaunch {
pub(super) plan: PreparedLaunch,
pub(super) resume: Option<managed_agents::remote_stop::ResumeTicket>,
pub(super) generation: Option<String>,
}
fn record(app: &AppHandle, agent: &str) -> Result<managed_agents::ManagedAgentRecord, String> {
let state = app.state::<AppState>();
let _store = state
@@ -19,16 +26,51 @@ pub(super) async fn prepare(
app: &AppHandle,
owner: &str,
request: &Request,
) -> Result<PreparedLaunch, Outcome> {
) -> Result<CapturedLaunch, Outcome> {
let reference = request.configuration.as_ref().ok_or(Outcome::Ineligible)?;
let record = record(app, &request.target.agent).map_err(|_| Outcome::Ineligible)?;
let plan = configurations::prepare_for_app(
app,
&record,
Some(reference),
owner,
&request.target.community,
)
let mut captured = (|| -> Result<CapturedLaunch, String> {
let state = app.state::<AppState>();
let _transition = state
.managed_agent_runtime_transition
.lock()
.map_err(|e| e.to_string())?;
scope(app, &state, owner, &request.target.community)?;
let record = record(app, &request.target.agent)?;
let plan = configurations::prepare_for_app(
app,
&record,
Some(reference),
owner,
&request.target.community,
)?;
let key = managed_agents::ManagedAgentRuntimeKey::new(
&request.target.agent,
&request.target.community,
)?;
// Only explicit Start can supersede an older Stop. Capture its existing
// shared fence before await; probes and Restart receive no resume authority.
let resume = if request.action == Action::Start {
Some(managed_agents::remote_stop::capture_resume(
app,
&key,
&request.target.community,
owner,
)?)
} else {
None
};
let generation = state
.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?
.get(&key)
.map(|runtime| runtime.start_nonce.clone());
Ok(CapturedLaunch {
plan,
resume,
generation,
})
})()
.map_err(|_| Outcome::Ineligible)?;
// Do not advertise an eligible Switch that is known to fail after source Stop.
broker_launch::provision(
@@ -37,20 +79,38 @@ pub(super) async fn prepare(
community: &request.target.community,
agent: &request.target.agent,
},
plan.record(),
captured.plan.record(),
)?;
configurations::preflight_prepared(app, &plan, owner, &request.target.community)
.await
.map_err(|_| Outcome::Ineligible)?;
Ok(plan)
configurations::preflight_with(
&mut captured.plan,
owner,
&request.target.community,
false,
|model, allow| async move {
#[cfg(feature = "mesh-llm")]
{
crate::commands::ensure_relay_mesh_for_record(app, model.as_deref(), allow).await
}
#[cfg(not(feature = "mesh-llm"))]
{
let _ = (app, model, allow);
Ok(())
}
},
)
.await
.map_err(|_| Outcome::Ineligible)?;
Ok(captured)
}
pub(super) fn revalidate(
app: &AppHandle,
owner: &str,
request: &Request,
plan: &PreparedLaunch,
captured: &CapturedLaunch,
) -> Result<(), String> {
let plan = &captured.plan;
plan.require_preflight()?;
plan.check_scope(Some(owner), &request.target.community)?;
if plan.configuration().as_ref() != request.configuration.as_ref() {
return Err("Prepared configuration does not match the request".into());

View File

@@ -463,8 +463,12 @@ confirms source Stop, then requests a fresh Start of that exact revision. Prefli
and Catalog cannot write placement intent or stop a process. The native consumer
uses ordinary asynchronous preflight outside the transition lock, revalidates the
immutable plan inside it, and delegates to the shared prepared launcher under the
existing admission/Stop fences. No second spawn path or launch authority is allowed.
Missing/edited targets fail rather than substitute another configuration. Remote
existing admission/Stop fences. Only explicit Start captures the shared Stop-fence
resume ticket before preflight; probes and Restart never receive that authority.
Shared admission checks the captured runtime generation before destructive Restart,
and a post-Stop Failed status must never become a Running response. No second spawn
path or launch authority is allowed. Missing/edited targets fail rather than
substitute another configuration. Remote
keyless provisioning remains its existing independent fail-closed gate; a known
provisioning failure must exclude Switch before source Stop.