Skip to content

Commit 0e2a9cf

Browse files
author
Chris Huber
committed
fix(runner): reconcile disconnected generation state (#11898)
AI assistance: OpenAI GPT-5.6 Sol via OpenCode inspected the runner recovery flow, implemented the guarded terminal settlement, and added deterministic command-contract coverage. Chris Huber remains responsible for every line.
1 parent 7d4fedd commit 0e2a9cf

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

crates/homeboy-lab-runner/src/connection.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,9 +1809,12 @@ pub fn reconcile_status(runner_id: &str) -> Result<RunnerStatusReport> {
18091809
if report.connected {
18101810
reconcile_session_metadata_with_observed_daemon(&runner, &mut report.session, true)?;
18111811
}
1812-
// Terminal-job settlement was formerly coupled to status. Keep it under
1813-
// this explicit lifecycle owner so status remains safe to parallelize.
1814-
reconcile_terminal_jobs(runner_id)?;
1812+
// Terminal-job settlement needs a live session transport. Generation
1813+
// reconciliation below can instead inspect a disconnected SSH runner, so
1814+
// do not let the terminal-job preflight block its advertised recovery path.
1815+
if report.connected {
1816+
reconcile_terminal_jobs(runner_id)?;
1817+
}
18151818
if let Ok(Some((_, _, client))) = remote_daemon::resolve_ssh_runner(&runner) {
18161819
super::generation_store::reconcile_with_ssh(runner_id, report.session.as_ref(), &client)?;
18171820
} else {

crates/homeboy-lab-runner/src/connection/tests/session.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,40 @@ fn full_status_projection_leaves_large_legacy_shared_state_and_observation_db_un
19311931
});
19321932
}
19331933

1934+
#[test]
1935+
fn disconnected_generation_reconcile_action_passes_reconcile_preflight() {
1936+
test_support::with_isolated_home(|_| {
1937+
crate::create(r#"{"id":"homeboy-lab","kind":"local"}"#, false).expect("create runner");
1938+
let mut session = direct_ssh_session("lease-stale");
1939+
// The persisted stale generation has no local tunnel to probe. This
1940+
// keeps the fixture focused on disconnected recovery preflight.
1941+
session.local_url = None;
1942+
session.local_port = None;
1943+
write_session(&session).expect("write disconnected direct session");
1944+
let mut generations = crate::RollingGenerations::new("lease-stale", session);
1945+
generations.admit();
1946+
crate::generation_store::write("homeboy-lab", &generations)
1947+
.expect("persist stale generation evidence");
1948+
1949+
let status = status("homeboy-lab").expect("read disconnected status");
1950+
assert!(!status.connected);
1951+
let inventory =
1952+
crate::generation_store::status_projection("homeboy-lab", status.session.as_ref())
1953+
.expect("read persisted generation evidence");
1954+
assert_eq!(
1955+
status
1956+
.admission_summary_with_generations(&inventory, &[], inventory.len())
1957+
.next_action
1958+
.as_deref(),
1959+
Some("homeboy runner reconcile homeboy-lab")
1960+
);
1961+
1962+
let reconciled = reconcile_status("homeboy-lab")
1963+
.expect("status-recommended reconciliation accepts a disconnected runner");
1964+
assert!(!reconciled.connected);
1965+
});
1966+
}
1967+
19341968
fn snapshot_directory(
19351969
root: &std::path::Path,
19361970
) -> std::collections::BTreeMap<std::path::PathBuf, Vec<u8>> {

0 commit comments

Comments
 (0)