Skip to content

Commit bfab7a7

Browse files
authored
Merge pull request #10502 from Extra-Chill/fix/10497-cook-controller-runtime
Fix cook continuation controller runtime identity
2 parents 9b9bb93 + 873bd3b commit bfab7a7

3 files changed

Lines changed: 111 additions & 11 deletions

File tree

crates/homeboy-agents/src/agent_task_lifecycle/lifecycle_ops.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -448,22 +448,32 @@ where
448448
record.metadata[key] = value.clone();
449449
}
450450
}
451-
if execution_runner_id.as_deref() == existing.runner_id() {
451+
if execution_runner_id.as_deref().is_some_and(|runner_id| {
452+
existing
453+
.runner_id()
454+
.is_none_or(|existing_runner_id| existing_runner_id == runner_id)
455+
}) {
452456
// A foreground daemon binds its job before launching runner-local
453457
// `run-plan`. Keep that transport identity when run-plan replaces
454458
// the staged record, or terminal projection cannot join its daemon
455459
// snapshot back to the completed agent-task run.
456460
if let Some(runner_job_id) = existing.runner_job_id() {
457461
record.metadata["runner_job_id"] = json!(runner_job_id);
458462
}
463+
// The runner can re-submit after its workspace has been reaped,
464+
// including before the handoff acceptance projection is durable.
465+
// Its local pin is execution evidence only; continuations remain
466+
// owned by the controller seat that created this record. Fail closed
467+
// if that controller pin is unavailable rather than replacing it
468+
// with the runner's host-local executable.
469+
preserved_controller_runtime = Some(controller_runtime_for_runner_execution(
470+
&existing,
471+
execution_runner_id.as_deref(),
472+
)?);
459473
if existing.lab_handoff.as_ref().is_some_and(|handoff| {
460474
handoff.state == AgentTaskLabHandoffState::Accepted
461475
&& handoff.authority == AgentTaskLabHandoffAuthority::RunnerDaemon
462476
}) {
463-
preserved_controller_runtime = Some(controller_runtime_for_runner_execution(
464-
&existing,
465-
execution_runner_id.as_deref(),
466-
)?);
467477
record.lab_handoff = existing.lab_handoff;
468478
}
469479
}
@@ -524,14 +534,12 @@ pub(crate) fn controller_runtime_for_runner_execution(
524534
existing: &AgentTaskRunRecord,
525535
execution_runner_id: Option<&str>,
526536
) -> Result<Value> {
527-
if execution_runner_id != existing.runner_id()
528-
|| !existing.lab_handoff.as_ref().is_some_and(|handoff| {
529-
handoff.state == AgentTaskLabHandoffState::Accepted
530-
&& handoff.authority == AgentTaskLabHandoffAuthority::RunnerDaemon
531-
})
537+
if existing
538+
.runner_id()
539+
.is_some_and(|runner_id| Some(runner_id) != execution_runner_id)
532540
{
533541
return Err(Error::internal_unexpected(
534-
"runner execution identity was requested for a non-accepted Lab handoff",
542+
"runner execution identity does not match the controller-owned Lab handoff",
535543
));
536544
}
537545
existing

crates/homeboy-agents/src/agent_task_lifecycle/tests/submit_and_persist.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,62 @@ fn accepted_lab_runner_execution_preserves_controller_runtime_pin_across_host_id
367367
});
368368
}
369369

370+
#[test]
371+
fn planned_lab_runner_execution_preserves_controller_runtime_pin_before_acceptance() {
372+
with_isolated_home(|_| {
373+
let run_id = "cross-host-controller-pin-planned";
374+
let controller_runtime = json!({
375+
"schema": "homeboy/controller-runtime-pin/v2",
376+
"originating": {
377+
"build_identity": "homeboy 0.300.0+macos",
378+
"pinned_executable": "/Users/chubes/.local/share/homeboy/controller-runtimes/macos/homeboy",
379+
"sha256": "macos-controller-sha256"
380+
}
381+
});
382+
let runner_runtime = json!({
383+
"schema": "homeboy/controller-runtime-pin/v2",
384+
"originating": {
385+
"build_identity": "homeboy 0.300.0+linux",
386+
"pinned_executable": "/home/chubes/.local/share/homeboy/controller-runtimes/linux/homeboy",
387+
"sha256": "linux-runner-sha256"
388+
}
389+
});
390+
let command = vec![
391+
"homeboy".to_string(),
392+
"agent-task".to_string(),
393+
"run-plan".to_string(),
394+
];
395+
396+
submit_plan_with_runtime_admission(&test_plan(), Some(run_id), |_| {
397+
Ok(controller_runtime.clone())
398+
})
399+
.expect("controller submits the durable run");
400+
record_lab_offload_planned(LabOffloadProxyPlan {
401+
run_id,
402+
runner_id: "linux-lab",
403+
remote_workspace: "/home/chubes/homeboy",
404+
remote_command: &command,
405+
durable_plan: None,
406+
})
407+
.expect("controller records planned handoff");
408+
409+
submit_plan_with_runtime_admission_on_runner(
410+
&test_plan(),
411+
Some(run_id),
412+
Some("linux-lab".to_string()),
413+
|_| Ok(runner_runtime.clone()),
414+
)
415+
.expect("runner preserves controller-seat runtime before acceptance");
416+
417+
let record = status(run_id).expect("controller record");
418+
assert_eq!(
419+
record.metadata[homeboy_core::controller_runtime::CONTROLLER_RUNTIME_METADATA_KEY],
420+
controller_runtime
421+
);
422+
assert_eq!(record.metadata["runner_execution_runtime"], runner_runtime);
423+
});
424+
}
425+
370426
#[test]
371427
fn accepted_lab_runner_execution_rejects_missing_controller_runtime_pin() {
372428
with_isolated_home(|_| {

crates/homeboy-agents/src/agent_task_service/cook_tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5189,6 +5189,42 @@ fn finalization_operation_claim_revalidates_completed_publication() {
51895189
});
51905190
}
51915191

5192+
#[test]
5193+
fn review_form_follow_up_finalization_replays_its_durable_claim_after_restart() {
5194+
homeboy_core::test_support::with_isolated_home(|_| {
5195+
let cook_id = "cook-review-form-restart";
5196+
let run_id = "cook-review-form-restart-attempt-2";
5197+
let plan = AgentTaskPlan::new(cook_id, Vec::new());
5198+
agent_task_lifecycle::submit_plan(&plan, Some(run_id)).unwrap();
5199+
agent_task_lifecycle::record_cook_attempt(cook_id, 2, run_id).unwrap();
5200+
let options = promotion_claim_options(cook_id, run_id);
5201+
let promotion = promotion(run_id);
5202+
let calls = Arc::new(AtomicUsize::new(0));
5203+
5204+
for _ in 0..2 {
5205+
let calls = Arc::clone(&calls);
5206+
let mut finalize =
5207+
move |_: &AgentTaskCookServiceOptions, _: &str, _: &AgentTaskPromotionReport| {
5208+
calls.fetch_add(1, Ordering::SeqCst);
5209+
Ok(serde_json::json!({"status": "review_ready", "review_form": true}))
5210+
};
5211+
finalize_with_operation_claim(&options, run_id, &promotion, &mut finalize).unwrap();
5212+
}
5213+
5214+
let operation_key = finalization_operation_key(run_id, &promotion);
5215+
let claim = agent_task_lifecycle::operation_claim(run_id, &operation_key)
5216+
.unwrap()
5217+
.expect("review-form finalization claim");
5218+
assert_eq!(claim.state, agent_task_lifecycle::ClaimState::Completed);
5219+
assert_eq!(claim.result.unwrap()["review_form"], true);
5220+
assert_eq!(
5221+
calls.load(Ordering::SeqCst),
5222+
2,
5223+
"restart only revalidates publication"
5224+
);
5225+
});
5226+
}
5227+
51925228
#[test]
51935229
fn duplicate_controller_passes_revalidate_one_promoted_candidate() {
51945230
// #8357 acceptance (AC5 + AC7): duplicate/concurrent controller passes over

0 commit comments

Comments
 (0)