@team-agent/installer 0.5.77 → 0.5.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -1303,11 +1303,12 @@ fn restart_with_selected_team_and_transport(
|
|
|
1303
1303
|
)?;
|
|
1304
1304
|
// 0.3.30 Bug 1: auto-attach on partial restart too — workers that did
|
|
1305
1305
|
// come up still need a leader_receiver pane to deliver report_result.
|
|
1306
|
-
let attach_window_failures =
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1306
|
+
let (attach_window_failures, leader_bind_ok, leader_bind_reason) =
|
|
1307
|
+
restart_leader_public_bind(
|
|
1308
|
+
&selected.run_workspace,
|
|
1309
|
+
Some(selected.team_key.as_str()),
|
|
1310
|
+
&state,
|
|
1311
|
+
);
|
|
1311
1312
|
return Ok(RestartReport::Partial {
|
|
1312
1313
|
session_name,
|
|
1313
1314
|
agents: successful_agents,
|
|
@@ -1317,6 +1318,8 @@ fn restart_with_selected_team_and_transport(
|
|
|
1317
1318
|
next_actions,
|
|
1318
1319
|
attach_commands,
|
|
1319
1320
|
attach_window_failures,
|
|
1321
|
+
leader_bind_ok,
|
|
1322
|
+
leader_bind_reason,
|
|
1320
1323
|
});
|
|
1321
1324
|
}
|
|
1322
1325
|
phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
|
|
@@ -1332,8 +1335,12 @@ fn restart_with_selected_team_and_transport(
|
|
|
1332
1335
|
// invoked from a tmux pane should bind that pane as leader_receiver,
|
|
1333
1336
|
// restoring the worker→leader delivery path. Failure is non-fatal — the
|
|
1334
1337
|
// user can still run `team-agent attach-leader` manually.
|
|
1335
|
-
let attach_window_failures =
|
|
1336
|
-
|
|
1338
|
+
let (attach_window_failures, leader_bind_ok, leader_bind_reason) =
|
|
1339
|
+
restart_leader_public_bind(
|
|
1340
|
+
&selected.run_workspace,
|
|
1341
|
+
Some(selected.team_key.as_str()),
|
|
1342
|
+
&state,
|
|
1343
|
+
);
|
|
1337
1344
|
if let Ok(probe) = crate::lifecycle::display::probe_display_capabilities(&selected.run_workspace)
|
|
1338
1345
|
{
|
|
1339
1346
|
let _ = crate::lifecycle::display::rebuild_adaptive_display_after_rebind(
|
|
@@ -1355,6 +1362,8 @@ fn restart_with_selected_team_and_transport(
|
|
|
1355
1362
|
next_actions,
|
|
1356
1363
|
attach_commands,
|
|
1357
1364
|
attach_window_failures,
|
|
1365
|
+
leader_bind_ok,
|
|
1366
|
+
leader_bind_reason,
|
|
1358
1367
|
})
|
|
1359
1368
|
}
|
|
1360
1369
|
|
|
@@ -1669,8 +1678,14 @@ fn restart_readiness(
|
|
|
1669
1678
|
let session_created = session_live_or_default(transport, session_name, false);
|
|
1670
1679
|
let worker_pane_addressable = restart_worker_panes_addressable(state, decisions, transport);
|
|
1671
1680
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
1672
|
-
let
|
|
1673
|
-
|
|
1681
|
+
let health = crate::coordinator::coordinator_health(&coordinator_workspace);
|
|
1682
|
+
let coordinator_accepted = health.ok
|
|
1683
|
+
|| (health.service_available
|
|
1684
|
+
&& matches!(
|
|
1685
|
+
health.binary_identity_relation,
|
|
1686
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
1687
|
+
));
|
|
1688
|
+
let coordinator_alive = coordinator_accepted && session_created;
|
|
1674
1689
|
RestartReadiness {
|
|
1675
1690
|
session_created,
|
|
1676
1691
|
worker_pane_addressable,
|
|
@@ -1772,13 +1787,14 @@ fn restart_readiness_timeout_message(
|
|
|
1772
1787
|
- tmux session created: {session}\n\
|
|
1773
1788
|
- worker pane addressable: {pane}\n\
|
|
1774
1789
|
- coordinator alive: {coordinator}\n\
|
|
1775
|
-
Action: check coordinator log {log}, then
|
|
1790
|
+
Action: check coordinator log {log}, then inspect this workspace with `team-agent diagnose --workspace '{workspace}'`\n\
|
|
1776
1791
|
Log: coordinator_log={log} state={state} pid_file={pid}",
|
|
1777
1792
|
missing = restart_readiness_missing_summary(readiness),
|
|
1778
1793
|
session = yes_no(readiness.session_created),
|
|
1779
1794
|
pane = yes_no(readiness.worker_pane_addressable),
|
|
1780
1795
|
coordinator = yes_no(readiness.coordinator_alive),
|
|
1781
1796
|
log = crate::coordinator::coordinator_log_path(&coordinator_workspace).display(),
|
|
1797
|
+
workspace = workspace.display(),
|
|
1782
1798
|
state = crate::state::persist::runtime_state_path(workspace).display(),
|
|
1783
1799
|
pid = crate::coordinator::coordinator_pid_path(&coordinator_workspace).display(),
|
|
1784
1800
|
)
|
|
@@ -1824,63 +1840,276 @@ fn verify_spawned_agent_live(
|
|
|
1824
1840
|
/// E51 guard). In that case the user must still run `attach-leader` manually,
|
|
1825
1841
|
/// matching pre-fix behaviour. We log to stderr so the operator sees why
|
|
1826
1842
|
/// auto-attach didn't take.
|
|
1843
|
+
fn load_restart_bind_state(
|
|
1844
|
+
workspace: &std::path::Path,
|
|
1845
|
+
team_key: &str,
|
|
1846
|
+
) -> Option<serde_json::Value> {
|
|
1847
|
+
crate::state::projection::select_runtime_state(workspace, Some(team_key)).ok()
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
fn selected_team_receiver<'a>(
|
|
1851
|
+
state: &'a serde_json::Value,
|
|
1852
|
+
team_key: &str,
|
|
1853
|
+
) -> Option<&'a serde_json::Value> {
|
|
1854
|
+
crate::lifecycle::launch::selected_team_leader_receiver(state, team_key)
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
fn transport_for_receiver(
|
|
1858
|
+
_workspace: &std::path::Path,
|
|
1859
|
+
receiver: &serde_json::Value,
|
|
1860
|
+
) -> Option<Box<dyn crate::transport::Transport>> {
|
|
1861
|
+
let endpoint = receiver
|
|
1862
|
+
.get("tmux_socket")
|
|
1863
|
+
.and_then(serde_json::Value::as_str)
|
|
1864
|
+
.filter(|endpoint| !endpoint.is_empty() && *endpoint != "default")?;
|
|
1865
|
+
Some(crate::transport_factory::leader_endpoint_transport(
|
|
1866
|
+
endpoint,
|
|
1867
|
+
))
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
pub fn restart_bind_status(
|
|
1871
|
+
workspace: &std::path::Path,
|
|
1872
|
+
team_key: &str,
|
|
1873
|
+
state: &serde_json::Value,
|
|
1874
|
+
) -> (bool, Option<String>) {
|
|
1875
|
+
let receiver = selected_team_receiver(state, team_key);
|
|
1876
|
+
let transport = receiver.and_then(|receiver| transport_for_receiver(workspace, receiver));
|
|
1877
|
+
match crate::lifecycle::launch::classify_leader_binding_at(
|
|
1878
|
+
workspace,
|
|
1879
|
+
team_key,
|
|
1880
|
+
state,
|
|
1881
|
+
transport.as_deref(),
|
|
1882
|
+
) {
|
|
1883
|
+
crate::lifecycle::launch::LeaderBindingClass::Attached => (true, None),
|
|
1884
|
+
other => (false, Some(other.issue_id().to_string())),
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
fn owner_conflicts_with_caller(
|
|
1889
|
+
workspace: &std::path::Path,
|
|
1890
|
+
state: &serde_json::Value,
|
|
1891
|
+
team_key: &str,
|
|
1892
|
+
) -> bool {
|
|
1893
|
+
let Some(receiver) = selected_team_receiver(state, team_key) else {
|
|
1894
|
+
return false;
|
|
1895
|
+
};
|
|
1896
|
+
let Some(owner_pane) = state
|
|
1897
|
+
.get("teams")
|
|
1898
|
+
.and_then(|teams| teams.get(team_key))
|
|
1899
|
+
.and_then(|team| team.pointer("/team_owner/pane_id"))
|
|
1900
|
+
.and_then(serde_json::Value::as_str)
|
|
1901
|
+
.or_else(|| receiver.get("pane_id").and_then(serde_json::Value::as_str))
|
|
1902
|
+
else {
|
|
1903
|
+
return false;
|
|
1904
|
+
};
|
|
1905
|
+
let Some(caller) = std::env::var("TMUX_PANE")
|
|
1906
|
+
.ok()
|
|
1907
|
+
.filter(|pane| !pane.is_empty())
|
|
1908
|
+
else {
|
|
1909
|
+
return false;
|
|
1910
|
+
};
|
|
1911
|
+
let owner_socket = receiver
|
|
1912
|
+
.get("tmux_socket")
|
|
1913
|
+
.and_then(serde_json::Value::as_str);
|
|
1914
|
+
let caller_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
1915
|
+
let same_socket = match (owner_socket, caller_socket.as_deref()) {
|
|
1916
|
+
(Some(owner), Some(caller)) => owner == caller,
|
|
1917
|
+
_ => false,
|
|
1918
|
+
};
|
|
1919
|
+
if owner_pane == caller && same_socket {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
let Some(transport) = transport_for_receiver(workspace, receiver) else {
|
|
1923
|
+
return false;
|
|
1924
|
+
};
|
|
1925
|
+
matches!(
|
|
1926
|
+
crate::messaging::leader_channel::resolve_live_leader_channel(
|
|
1927
|
+
workspace,
|
|
1928
|
+
receiver,
|
|
1929
|
+
transport.as_ref(),
|
|
1930
|
+
),
|
|
1931
|
+
crate::messaging::leader_channel::LeaderChannelResolution::Live(_)
|
|
1932
|
+
)
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
fn binding_maintain_failed(value: &serde_json::Value) -> bool {
|
|
1936
|
+
value
|
|
1937
|
+
.get("registry_maintain_failed")
|
|
1938
|
+
.and_then(serde_json::Value::as_bool)
|
|
1939
|
+
== Some(true)
|
|
1940
|
+
|| value
|
|
1941
|
+
.get("debt_maintain_failed")
|
|
1942
|
+
.and_then(serde_json::Value::as_bool)
|
|
1943
|
+
== Some(true)
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
fn apply_binding_maintain_failures(
|
|
1947
|
+
attach_window_failures: &Option<serde_json::Value>,
|
|
1948
|
+
ok: bool,
|
|
1949
|
+
reason: Option<String>,
|
|
1950
|
+
) -> (bool, Option<String>) {
|
|
1951
|
+
if attach_window_failures
|
|
1952
|
+
.as_ref()
|
|
1953
|
+
.is_some_and(binding_maintain_failed)
|
|
1954
|
+
{
|
|
1955
|
+
(
|
|
1956
|
+
false,
|
|
1957
|
+
reason.or_else(|| Some("leader_registry_maintain_failed".to_string())),
|
|
1958
|
+
)
|
|
1959
|
+
} else {
|
|
1960
|
+
(ok, reason)
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/// Unique production constructor for Restarted/Partial `leader_bind_*`
|
|
1965
|
+
/// and `attach_window_failures`. `restart_with_selected_team` is the only
|
|
1966
|
+
/// product caller; IsolatedHome reuses this instead of a test success path.
|
|
1967
|
+
pub fn restart_leader_public_bind(
|
|
1968
|
+
workspace: &std::path::Path,
|
|
1969
|
+
team: Option<&str>,
|
|
1970
|
+
in_memory_state: &serde_json::Value,
|
|
1971
|
+
) -> (Option<serde_json::Value>, bool, Option<String>) {
|
|
1972
|
+
let team_key = team.unwrap_or("");
|
|
1973
|
+
let attach_window_failures =
|
|
1974
|
+
try_autobind_leader_after_restart(workspace, team, in_memory_state);
|
|
1975
|
+
let persisted = load_restart_bind_state(workspace, team_key);
|
|
1976
|
+
let (class_ok, class_reason) = restart_bind_status(
|
|
1977
|
+
workspace,
|
|
1978
|
+
team_key,
|
|
1979
|
+
persisted.as_ref().unwrap_or(in_memory_state),
|
|
1980
|
+
);
|
|
1981
|
+
let (ok, reason) =
|
|
1982
|
+
apply_binding_maintain_failures(&attach_window_failures, class_ok, class_reason);
|
|
1983
|
+
(attach_window_failures, ok, reason)
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
fn maintain_live_binding_index(
|
|
1987
|
+
workspace: &std::path::Path,
|
|
1988
|
+
team: Option<&str>,
|
|
1989
|
+
state: &serde_json::Value,
|
|
1990
|
+
) -> Option<serde_json::Value> {
|
|
1991
|
+
let team_key = team.unwrap_or("");
|
|
1992
|
+
let register = crate::leader::registry::register_binding_from_state_best_effort(
|
|
1993
|
+
workspace,
|
|
1994
|
+
team,
|
|
1995
|
+
"restart-live-index",
|
|
1996
|
+
);
|
|
1997
|
+
let mut report = serde_json::Map::new();
|
|
1998
|
+
if !register
|
|
1999
|
+
.as_ref()
|
|
2000
|
+
.is_some_and(|outcome| outcome.status == "registered")
|
|
2001
|
+
{
|
|
2002
|
+
report.insert("registry_maintain_failed".to_string(), serde_json::json!(true));
|
|
2003
|
+
}
|
|
2004
|
+
let Some(receiver) = selected_team_receiver(state, team_key) else {
|
|
2005
|
+
return Some(serde_json::Value::Object(report));
|
|
2006
|
+
};
|
|
2007
|
+
let Some(pane) = receiver
|
|
2008
|
+
.get("pane_id")
|
|
2009
|
+
.and_then(serde_json::Value::as_str)
|
|
2010
|
+
.filter(|pane| !pane.is_empty())
|
|
2011
|
+
else {
|
|
2012
|
+
report.insert("debt_maintain_failed".to_string(), serde_json::json!(true));
|
|
2013
|
+
return Some(serde_json::Value::Object(report));
|
|
2014
|
+
};
|
|
2015
|
+
let store = match crate::message_store::MessageStore::open(workspace) {
|
|
2016
|
+
Ok(store) => store,
|
|
2017
|
+
Err(_) => {
|
|
2018
|
+
report.insert("debt_maintain_failed".to_string(), serde_json::json!(true));
|
|
2019
|
+
return Some(serde_json::Value::Object(report));
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
2023
|
+
let team_id = crate::model::ids::TeamKey::new(team_key.to_string());
|
|
2024
|
+
let pane_id = crate::transport::PaneId::new(pane);
|
|
2025
|
+
match crate::messaging::watchers::requeue_delivery_exhausted_watchers(
|
|
2026
|
+
workspace,
|
|
2027
|
+
&store,
|
|
2028
|
+
&event_log,
|
|
2029
|
+
&team_id,
|
|
2030
|
+
&pane_id,
|
|
2031
|
+
) {
|
|
2032
|
+
Ok(notices) => {
|
|
2033
|
+
report.insert("notices".to_string(), serde_json::json!(notices.len()));
|
|
2034
|
+
}
|
|
2035
|
+
Err(_) => {
|
|
2036
|
+
report.insert("debt_maintain_failed".to_string(), serde_json::json!(true));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
Some(serde_json::Value::Object(report))
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1827
2042
|
fn try_autobind_leader_after_restart(
|
|
1828
2043
|
workspace: &std::path::Path,
|
|
1829
2044
|
team: Option<&str>,
|
|
1830
2045
|
state: &serde_json::Value,
|
|
1831
2046
|
) -> Option<serde_json::Value> {
|
|
2047
|
+
let team_key = team.unwrap_or("");
|
|
2048
|
+
let persisted = load_restart_bind_state(workspace, team_key);
|
|
2049
|
+
let state = persisted.as_ref().unwrap_or(state);
|
|
2050
|
+
if restart_bind_status(workspace, team_key, state).0 {
|
|
2051
|
+
return maintain_live_binding_index(workspace, team, state);
|
|
2052
|
+
}
|
|
1832
2053
|
if std::env::var_os("TMUX_PANE").is_none() {
|
|
1833
2054
|
return None;
|
|
1834
2055
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2056
|
+
if owner_conflicts_with_caller(workspace, state, team_key) {
|
|
2057
|
+
eprintln!(
|
|
2058
|
+
"team_agent::restart auto_attach_leader skipped reason=owner_conflict team={team:?}"
|
|
2059
|
+
);
|
|
2060
|
+
return None;
|
|
2061
|
+
}
|
|
2062
|
+
let Some(provider) = state
|
|
2063
|
+
.get("teams")
|
|
2064
|
+
.and_then(|teams| teams.get(team_key))
|
|
2065
|
+
.and_then(|team| team.pointer("/team_owner/provider"))
|
|
1840
2066
|
.and_then(serde_json::Value::as_str)
|
|
1841
2067
|
.or_else(|| {
|
|
1842
|
-
state
|
|
1843
|
-
.
|
|
2068
|
+
selected_team_receiver(state, team_key)
|
|
2069
|
+
.and_then(|receiver| receiver.get("provider"))
|
|
1844
2070
|
.and_then(serde_json::Value::as_str)
|
|
1845
2071
|
})
|
|
1846
|
-
.and_then(
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
})
|
|
1855
|
-
})
|
|
1856
|
-
.unwrap_or(crate::model::enums::Provider::ClaudeCode);
|
|
1857
|
-
let team_str = team;
|
|
1858
|
-
match crate::leader::attach_leader(workspace, team_str, None, provider) {
|
|
2072
|
+
.and_then(crate::provider::wire::parse_provider)
|
|
2073
|
+
else {
|
|
2074
|
+
eprintln!(
|
|
2075
|
+
"team_agent::restart auto_attach_leader skipped reason=provider_unresolved team={team:?}"
|
|
2076
|
+
);
|
|
2077
|
+
return None;
|
|
2078
|
+
};
|
|
2079
|
+
match crate::leader::attach_leader(workspace, team, None, provider) {
|
|
1859
2080
|
Ok(result) if result.ok => {
|
|
1860
|
-
let debt = result
|
|
1861
|
-
|
|
2081
|
+
let mut debt = result
|
|
2082
|
+
.attach_window_failures
|
|
2083
|
+
.clone()
|
|
2084
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
2085
|
+
let register = crate::leader::registry::register_binding_from_state_best_effort(
|
|
1862
2086
|
workspace,
|
|
1863
|
-
|
|
2087
|
+
team,
|
|
1864
2088
|
"restart-auto-attach",
|
|
1865
2089
|
);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2090
|
+
if !register
|
|
2091
|
+
.as_ref()
|
|
2092
|
+
.is_some_and(|outcome| outcome.status == "registered")
|
|
2093
|
+
{
|
|
2094
|
+
if let Some(object) = debt.as_object_mut() {
|
|
2095
|
+
object.insert(
|
|
2096
|
+
"registry_maintain_failed".to_string(),
|
|
2097
|
+
serde_json::json!(true),
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
Some(debt)
|
|
1872
2102
|
}
|
|
1873
2103
|
Ok(result) => {
|
|
1874
2104
|
eprintln!(
|
|
1875
|
-
"team_agent::restart auto_attach_leader skipped reason={:?} team={:?}",
|
|
1876
|
-
result.reason,
|
|
2105
|
+
"team_agent::restart auto_attach_leader skipped reason={:?} team={team:?}",
|
|
2106
|
+
result.reason,
|
|
1877
2107
|
);
|
|
1878
2108
|
None
|
|
1879
2109
|
}
|
|
1880
2110
|
Err(error) => {
|
|
1881
2111
|
eprintln!(
|
|
1882
|
-
"team_agent::restart auto_attach_leader failed error={error} team={
|
|
1883
|
-
(run `team-agent attach-leader` from your tmux pane to bind manually)",
|
|
2112
|
+
"team_agent::restart auto_attach_leader failed error={error} team={team:?}"
|
|
1884
2113
|
);
|
|
1885
2114
|
None
|
|
1886
2115
|
}
|
|
@@ -2529,10 +2758,10 @@ fn write_restart_agent_failed_event(
|
|
|
2529
2758
|
"session_id": decision.session_id.as_ref().map(|session| session.as_str()),
|
|
2530
2759
|
"phase": phase,
|
|
2531
2760
|
"error": error,
|
|
2532
|
-
"action":
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
decision.
|
|
2761
|
+
"action": format!(
|
|
2762
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
2763
|
+
decision.agent_id,
|
|
2764
|
+
decision.agent_id
|
|
2536
2765
|
),
|
|
2537
2766
|
"log": format!(
|
|
2538
2767
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3015,30 +3244,14 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
|
|
|
3015
3244
|
failed_agents
|
|
3016
3245
|
.iter()
|
|
3017
3246
|
.map(|failure| {
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
failure.session_id.as_ref(),
|
|
3247
|
+
format!(
|
|
3248
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
3249
|
+
failure.agent_id, failure.agent_id
|
|
3022
3250
|
)
|
|
3023
3251
|
})
|
|
3024
3252
|
.collect()
|
|
3025
3253
|
}
|
|
3026
3254
|
|
|
3027
|
-
pub(crate) fn restart_failure_action(
|
|
3028
|
-
agent_id: &AgentId,
|
|
3029
|
-
phase: &str,
|
|
3030
|
-
session_id: Option<&SessionId>,
|
|
3031
|
-
) -> String {
|
|
3032
|
-
if phase == "spawn" && session_id.is_none() {
|
|
3033
|
-
return format!(
|
|
3034
|
-
"worker {agent_id} failed before a pane was created; inspect the reported provider error, then rerun `team-agent restart --allow-fresh`"
|
|
3035
|
-
);
|
|
3036
|
-
}
|
|
3037
|
-
format!(
|
|
3038
|
-
"inspect worker {agent_id} output, then reset that worker with `team-agent reset-agent {agent_id}` or rerun `team-agent restart --allow-fresh`"
|
|
3039
|
-
)
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
3255
|
fn start_mode_wire(mode: StartMode) -> &'static str {
|
|
3043
3256
|
match mode {
|
|
3044
3257
|
StartMode::Resumed => "resumed",
|
|
@@ -4007,4 +4220,721 @@ tasks:
|
|
|
4007
4220
|
);
|
|
4008
4221
|
}
|
|
4009
4222
|
}
|
|
4223
|
+
|
|
4224
|
+
#[test]
|
|
4225
|
+
#[serial_test::serial(env)]
|
|
4226
|
+
fn restart_bind_status_live_index_missing_is_not_ok() {
|
|
4227
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4228
|
+
"ta-n1-index-{}-{}",
|
|
4229
|
+
std::process::id(),
|
|
4230
|
+
std::time::SystemTime::now()
|
|
4231
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4232
|
+
.unwrap()
|
|
4233
|
+
.as_nanos()
|
|
4234
|
+
));
|
|
4235
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4236
|
+
let home = workspace.join("home");
|
|
4237
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4238
|
+
let previous = std::env::var_os("HOME");
|
|
4239
|
+
std::env::set_var("HOME", &home);
|
|
4240
|
+
let state = serde_json::json!({
|
|
4241
|
+
"teams": {
|
|
4242
|
+
"alpha": {
|
|
4243
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 1, "provider": "codex"},
|
|
4244
|
+
"leader_receiver": {
|
|
4245
|
+
"status": "attached",
|
|
4246
|
+
"pane_id": "%1",
|
|
4247
|
+
"owner_epoch": 1,
|
|
4248
|
+
"tmux_socket": "/tmp/leader-not-workspace.sock"
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
});
|
|
4253
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
4254
|
+
let (ok, reason) = restart_bind_status(&workspace, "alpha", &state);
|
|
4255
|
+
match previous {
|
|
4256
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4257
|
+
None => std::env::remove_var("HOME"),
|
|
4258
|
+
}
|
|
4259
|
+
assert!(!ok, "Live/index-missing must not report bind success");
|
|
4260
|
+
assert_eq!(reason.as_deref(), Some("leader_registry_index_missing"));
|
|
4261
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
#[test]
|
|
4265
|
+
#[serial_test::serial(env)]
|
|
4266
|
+
fn restart_bind_status_uses_persisted_receiver_endpoint() {
|
|
4267
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4268
|
+
"ta-n1-persisted-{}-{}",
|
|
4269
|
+
std::process::id(),
|
|
4270
|
+
std::time::SystemTime::now()
|
|
4271
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4272
|
+
.unwrap()
|
|
4273
|
+
.as_nanos()
|
|
4274
|
+
));
|
|
4275
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4276
|
+
let home = workspace.join("home");
|
|
4277
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4278
|
+
let previous = std::env::var_os("HOME");
|
|
4279
|
+
std::env::set_var("HOME", &home);
|
|
4280
|
+
let state = serde_json::json!({
|
|
4281
|
+
"teams": {
|
|
4282
|
+
"alpha": {
|
|
4283
|
+
"leader_receiver": {
|
|
4284
|
+
"status": "pending",
|
|
4285
|
+
"pane_id": "%old"
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
});
|
|
4290
|
+
let persisted = serde_json::json!({
|
|
4291
|
+
"teams": {
|
|
4292
|
+
"alpha": {
|
|
4293
|
+
"team_owner": {"pane_id": "%2", "owner_epoch": 2, "provider": "codex"},
|
|
4294
|
+
"leader_receiver": {
|
|
4295
|
+
"status": "attached",
|
|
4296
|
+
"pane_id": "%2",
|
|
4297
|
+
"owner_epoch": 2,
|
|
4298
|
+
"tmux_socket": "/tmp/caller-socket.sock"
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
let (ok_stale, reason_stale) = restart_bind_status(&workspace, "alpha", &state);
|
|
4304
|
+
let (ok_new, reason_new) = restart_bind_status(&workspace, "alpha", &persisted);
|
|
4305
|
+
match previous {
|
|
4306
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4307
|
+
None => std::env::remove_var("HOME"),
|
|
4308
|
+
}
|
|
4309
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
4310
|
+
assert!(!ok_stale, "pending without owner/registry is not bind success");
|
|
4311
|
+
assert_eq!(
|
|
4312
|
+
reason_stale.as_deref(),
|
|
4313
|
+
Some("leader_receiver_unbound"),
|
|
4314
|
+
"hermetic empty registry + pending receiver is Unbound, not Unknown"
|
|
4315
|
+
);
|
|
4316
|
+
assert!(!ok_new);
|
|
4317
|
+
assert_eq!(
|
|
4318
|
+
reason_new.as_deref(),
|
|
4319
|
+
Some("leader_registry_index_missing"),
|
|
4320
|
+
"persisted attached+owner without registry is index-missing, not the stale pending class"
|
|
4321
|
+
);
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
#[test]
|
|
4325
|
+
#[serial_test::serial(env)]
|
|
4326
|
+
fn restart_bind_status_attached_live_is_ok() {
|
|
4327
|
+
let root = std::env::temp_dir().join(format!(
|
|
4328
|
+
"ta-n1-ok-{}-{}",
|
|
4329
|
+
std::process::id(),
|
|
4330
|
+
std::time::SystemTime::now()
|
|
4331
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4332
|
+
.unwrap()
|
|
4333
|
+
.as_nanos()
|
|
4334
|
+
));
|
|
4335
|
+
let home = root.join("home");
|
|
4336
|
+
let workspace = root.join("ws");
|
|
4337
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4338
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4339
|
+
let previous = std::env::var_os("HOME");
|
|
4340
|
+
std::env::set_var("HOME", &home);
|
|
4341
|
+
let endpoint = "/tmp/ta-n1-leader-not-workspace.sock";
|
|
4342
|
+
let pending = serde_json::json!({
|
|
4343
|
+
"teams": {
|
|
4344
|
+
"alpha": {
|
|
4345
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 1, "provider": "codex"},
|
|
4346
|
+
"leader_receiver": {
|
|
4347
|
+
"status": "pending",
|
|
4348
|
+
"discovery": "quick_start_seed",
|
|
4349
|
+
"pane_id": "%1",
|
|
4350
|
+
"owner_epoch": 1,
|
|
4351
|
+
"tmux_socket": endpoint
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
let attached = serde_json::json!({
|
|
4357
|
+
"teams": {
|
|
4358
|
+
"alpha": {
|
|
4359
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 1, "provider": "codex"},
|
|
4360
|
+
"leader_receiver": {
|
|
4361
|
+
"status": "attached",
|
|
4362
|
+
"pane_id": "%1",
|
|
4363
|
+
"owner_epoch": 1,
|
|
4364
|
+
"tmux_socket": endpoint
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
});
|
|
4369
|
+
crate::state::persist::save_runtime_state(&workspace, &attached).unwrap();
|
|
4370
|
+
assert_eq!(
|
|
4371
|
+
crate::leader::registry::register_binding_from_state_best_effort(
|
|
4372
|
+
&workspace,
|
|
4373
|
+
Some("alpha"),
|
|
4374
|
+
"restart-ok",
|
|
4375
|
+
)
|
|
4376
|
+
.as_ref()
|
|
4377
|
+
.map(|outcome| outcome.status),
|
|
4378
|
+
Some("registered")
|
|
4379
|
+
);
|
|
4380
|
+
let transport = crate::transport::test_support::OfflineTransport::default()
|
|
4381
|
+
.with_tmux_endpoint(endpoint)
|
|
4382
|
+
.with_targets(vec![crate::transport::PaneInfo {
|
|
4383
|
+
pane_id: crate::transport::PaneId::new("%1"),
|
|
4384
|
+
session: crate::transport::SessionName::new("s"),
|
|
4385
|
+
window_index: None,
|
|
4386
|
+
window_name: None,
|
|
4387
|
+
pane_index: None,
|
|
4388
|
+
tty: None,
|
|
4389
|
+
current_command: Some("codex".to_string()),
|
|
4390
|
+
current_path: Some(workspace.clone()),
|
|
4391
|
+
active: true,
|
|
4392
|
+
pane_pid: None,
|
|
4393
|
+
leader_env: Default::default(),
|
|
4394
|
+
}]);
|
|
4395
|
+
let (ok, reason) = crate::transport_factory::with_leader_endpoint_transport(
|
|
4396
|
+
endpoint,
|
|
4397
|
+
transport.clone(),
|
|
4398
|
+
|| restart_bind_status(&workspace, "alpha", &attached),
|
|
4399
|
+
);
|
|
4400
|
+
let (ok_pending, _) = crate::transport_factory::with_leader_endpoint_transport(
|
|
4401
|
+
endpoint,
|
|
4402
|
+
transport,
|
|
4403
|
+
|| restart_bind_status(&workspace, "alpha", &pending),
|
|
4404
|
+
);
|
|
4405
|
+
match previous {
|
|
4406
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4407
|
+
None => std::env::remove_var("HOME"),
|
|
4408
|
+
}
|
|
4409
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
4410
|
+
assert_eq!((ok, reason.as_deref()), (true, None));
|
|
4411
|
+
assert!(
|
|
4412
|
+
!ok_pending,
|
|
4413
|
+
"in-memory pending snapshot must not flip restart success"
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
fn restart_index_owner_state(
|
|
4418
|
+
pane: &str,
|
|
4419
|
+
endpoint: &str,
|
|
4420
|
+
uuid: &str,
|
|
4421
|
+
provider: &str,
|
|
4422
|
+
) -> serde_json::Value {
|
|
4423
|
+
serde_json::json!({
|
|
4424
|
+
"teams": {
|
|
4425
|
+
"alpha": {
|
|
4426
|
+
"team_owner": {
|
|
4427
|
+
"pane_id": pane,
|
|
4428
|
+
"owner_epoch": 1,
|
|
4429
|
+
"provider": provider,
|
|
4430
|
+
"leader_session_uuid": uuid
|
|
4431
|
+
},
|
|
4432
|
+
"leader_receiver": {
|
|
4433
|
+
"status": "attached",
|
|
4434
|
+
"pane_id": pane,
|
|
4435
|
+
"owner_epoch": 1,
|
|
4436
|
+
"provider": provider,
|
|
4437
|
+
"tmux_socket": endpoint,
|
|
4438
|
+
"leader_session_uuid": uuid,
|
|
4439
|
+
"discovery": "env_pane"
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
})
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
fn restart_index_wrapper_pane(
|
|
4447
|
+
workspace: &Path,
|
|
4448
|
+
pane: &str,
|
|
4449
|
+
uuid: Option<&str>,
|
|
4450
|
+
active: bool,
|
|
4451
|
+
command: &str,
|
|
4452
|
+
) -> PaneInfo {
|
|
4453
|
+
let mut leader_env = BTreeMap::new();
|
|
4454
|
+
if let Some(uuid) = uuid {
|
|
4455
|
+
leader_env.insert(
|
|
4456
|
+
"TEAM_AGENT_LEADER_SESSION_UUID".to_string(),
|
|
4457
|
+
uuid.to_string(),
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
PaneInfo {
|
|
4461
|
+
pane_id: crate::transport::PaneId::new(pane),
|
|
4462
|
+
session: crate::transport::SessionName::new("leader"),
|
|
4463
|
+
window_index: Some(0),
|
|
4464
|
+
window_name: Some(crate::transport::WindowName::new("pi")),
|
|
4465
|
+
pane_index: Some(0),
|
|
4466
|
+
tty: None,
|
|
4467
|
+
current_command: Some(command.to_string()),
|
|
4468
|
+
current_path: Some(workspace.to_path_buf()),
|
|
4469
|
+
active,
|
|
4470
|
+
pane_pid: Some(42),
|
|
4471
|
+
leader_env,
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
fn restart_index_bind_after_unregister(
|
|
4476
|
+
workspace: &Path,
|
|
4477
|
+
endpoint: &str,
|
|
4478
|
+
pane: &str,
|
|
4479
|
+
targets: Vec<PaneInfo>,
|
|
4480
|
+
) -> (
|
|
4481
|
+
(bool, Option<String>),
|
|
4482
|
+
crate::lifecycle::launch::LeaderBindingClass,
|
|
4483
|
+
) {
|
|
4484
|
+
std::env::set_var("TMUX", format!("{endpoint},1,0"));
|
|
4485
|
+
std::env::set_var("TMUX_PANE", pane);
|
|
4486
|
+
let transport = crate::transport::test_support::OfflineTransport::default()
|
|
4487
|
+
.with_tmux_endpoint(endpoint)
|
|
4488
|
+
.with_targets(targets);
|
|
4489
|
+
crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
|
|
4490
|
+
let (_failures, ok, reason) =
|
|
4491
|
+
restart_leader_public_bind(workspace, Some("alpha"), &serde_json::json!({}));
|
|
4492
|
+
let class = crate::lifecycle::launch::classify_leader_binding(workspace, "alpha");
|
|
4493
|
+
((ok, reason), class)
|
|
4494
|
+
})
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
#[test]
|
|
4498
|
+
#[serial_test::serial(env)]
|
|
4499
|
+
fn restart_rebinds_inactive_pi_bash_wrapper_after_unregister() {
|
|
4500
|
+
let root = std::env::temp_dir().join(format!(
|
|
4501
|
+
"ta-r14-idx-ok-{}-{}",
|
|
4502
|
+
std::process::id(),
|
|
4503
|
+
std::time::SystemTime::now()
|
|
4504
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4505
|
+
.unwrap()
|
|
4506
|
+
.as_nanos()
|
|
4507
|
+
));
|
|
4508
|
+
let home = root.join("home");
|
|
4509
|
+
let workspace = root.join("ws");
|
|
4510
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4511
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4512
|
+
let previous_home = std::env::var_os("HOME");
|
|
4513
|
+
let previous_tmux = std::env::var_os("TMUX");
|
|
4514
|
+
let previous_pane = std::env::var_os("TMUX_PANE");
|
|
4515
|
+
std::env::set_var("HOME", &home);
|
|
4516
|
+
let endpoint = "/tmp/ta-r14-idx-ok.sock";
|
|
4517
|
+
let uuid = "c1e3f7a7aeaed95491667b4d9c3ce6fb";
|
|
4518
|
+
let state = restart_index_owner_state("%0", endpoint, uuid, "codex");
|
|
4519
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
4520
|
+
assert_eq!(
|
|
4521
|
+
crate::leader::registry::register_binding_from_state_best_effort(
|
|
4522
|
+
&workspace,
|
|
4523
|
+
Some("alpha"),
|
|
4524
|
+
"quick-start",
|
|
4525
|
+
)
|
|
4526
|
+
.as_ref()
|
|
4527
|
+
.map(|outcome| outcome.status),
|
|
4528
|
+
Some("registered")
|
|
4529
|
+
);
|
|
4530
|
+
crate::leader::registry::unregister_entry(&workspace, "alpha");
|
|
4531
|
+
let ((ok, reason), class) = restart_index_bind_after_unregister(
|
|
4532
|
+
&workspace,
|
|
4533
|
+
endpoint,
|
|
4534
|
+
"%0",
|
|
4535
|
+
vec![restart_index_wrapper_pane(
|
|
4536
|
+
&workspace,
|
|
4537
|
+
"%0",
|
|
4538
|
+
Some(uuid),
|
|
4539
|
+
false,
|
|
4540
|
+
"bash",
|
|
4541
|
+
)],
|
|
4542
|
+
);
|
|
4543
|
+
match previous_home {
|
|
4544
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4545
|
+
None => std::env::remove_var("HOME"),
|
|
4546
|
+
}
|
|
4547
|
+
match previous_tmux {
|
|
4548
|
+
Some(value) => std::env::set_var("TMUX", value),
|
|
4549
|
+
None => std::env::remove_var("TMUX"),
|
|
4550
|
+
}
|
|
4551
|
+
match previous_pane {
|
|
4552
|
+
Some(value) => std::env::set_var("TMUX_PANE", value),
|
|
4553
|
+
None => std::env::remove_var("TMUX_PANE"),
|
|
4554
|
+
}
|
|
4555
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
4556
|
+
assert_eq!((ok, reason.as_deref()), (true, None));
|
|
4557
|
+
assert_eq!(class, crate::lifecycle::launch::LeaderBindingClass::Attached);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
#[test]
|
|
4561
|
+
#[serial_test::serial(env)]
|
|
4562
|
+
fn restart_index_missing_refuses_dead_and_wrong_identity() {
|
|
4563
|
+
let root = std::env::temp_dir().join(format!(
|
|
4564
|
+
"ta-r14-idx-neg-{}-{}",
|
|
4565
|
+
std::process::id(),
|
|
4566
|
+
std::time::SystemTime::now()
|
|
4567
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4568
|
+
.unwrap()
|
|
4569
|
+
.as_nanos()
|
|
4570
|
+
));
|
|
4571
|
+
let home = root.join("home");
|
|
4572
|
+
let workspace = root.join("ws");
|
|
4573
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4574
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4575
|
+
let previous_home = std::env::var_os("HOME");
|
|
4576
|
+
let previous_tmux = std::env::var_os("TMUX");
|
|
4577
|
+
let previous_pane = std::env::var_os("TMUX_PANE");
|
|
4578
|
+
std::env::set_var("HOME", &home);
|
|
4579
|
+
let endpoint = "/tmp/ta-r14-idx-neg.sock";
|
|
4580
|
+
let uuid = "c1e3f7a7aeaed95491667b4d9c3ce6fb";
|
|
4581
|
+
let state = restart_index_owner_state("%0", endpoint, uuid, "codex");
|
|
4582
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
4583
|
+
crate::leader::registry::unregister_entry(&workspace, "alpha");
|
|
4584
|
+
|
|
4585
|
+
let (dead, _) = restart_index_bind_after_unregister(&workspace, endpoint, "%0", Vec::new());
|
|
4586
|
+
let (wrong_pane, _) = restart_index_bind_after_unregister(
|
|
4587
|
+
&workspace,
|
|
4588
|
+
endpoint,
|
|
4589
|
+
"%9",
|
|
4590
|
+
vec![restart_index_wrapper_pane(
|
|
4591
|
+
&workspace,
|
|
4592
|
+
"%9",
|
|
4593
|
+
Some(uuid),
|
|
4594
|
+
false,
|
|
4595
|
+
"bash",
|
|
4596
|
+
)],
|
|
4597
|
+
);
|
|
4598
|
+
let wrong_socket = {
|
|
4599
|
+
std::env::set_var("TMUX", "/tmp/other.sock,1,0");
|
|
4600
|
+
std::env::set_var("TMUX_PANE", "%0");
|
|
4601
|
+
let transport = crate::transport::test_support::OfflineTransport::default()
|
|
4602
|
+
.with_tmux_endpoint("/tmp/other.sock")
|
|
4603
|
+
.with_targets(vec![restart_index_wrapper_pane(
|
|
4604
|
+
&workspace,
|
|
4605
|
+
"%0",
|
|
4606
|
+
Some(uuid),
|
|
4607
|
+
false,
|
|
4608
|
+
"bash",
|
|
4609
|
+
)]);
|
|
4610
|
+
crate::transport_factory::with_leader_endpoint_transport(
|
|
4611
|
+
"/tmp/other.sock",
|
|
4612
|
+
transport,
|
|
4613
|
+
|| {
|
|
4614
|
+
let (_failures, ok, reason) = restart_leader_public_bind(
|
|
4615
|
+
&workspace,
|
|
4616
|
+
Some("alpha"),
|
|
4617
|
+
&serde_json::json!({}),
|
|
4618
|
+
);
|
|
4619
|
+
(ok, reason)
|
|
4620
|
+
},
|
|
4621
|
+
)
|
|
4622
|
+
};
|
|
4623
|
+
let (wrong_uuid, _) = restart_index_bind_after_unregister(
|
|
4624
|
+
&workspace,
|
|
4625
|
+
endpoint,
|
|
4626
|
+
"%0",
|
|
4627
|
+
vec![restart_index_wrapper_pane(
|
|
4628
|
+
&workspace,
|
|
4629
|
+
"%0",
|
|
4630
|
+
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
|
4631
|
+
false,
|
|
4632
|
+
"bash",
|
|
4633
|
+
)],
|
|
4634
|
+
);
|
|
4635
|
+
match previous_home {
|
|
4636
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4637
|
+
None => std::env::remove_var("HOME"),
|
|
4638
|
+
}
|
|
4639
|
+
match previous_tmux {
|
|
4640
|
+
Some(value) => std::env::set_var("TMUX", value),
|
|
4641
|
+
None => std::env::remove_var("TMUX"),
|
|
4642
|
+
}
|
|
4643
|
+
match previous_pane {
|
|
4644
|
+
Some(value) => std::env::set_var("TMUX_PANE", value),
|
|
4645
|
+
None => std::env::remove_var("TMUX_PANE"),
|
|
4646
|
+
}
|
|
4647
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
4648
|
+
assert!(!dead.0);
|
|
4649
|
+
assert_eq!(dead.1.as_deref(), Some("leader_registry_index_missing"));
|
|
4650
|
+
assert!(!wrong_pane.0);
|
|
4651
|
+
assert_eq!(
|
|
4652
|
+
wrong_pane.1.as_deref(),
|
|
4653
|
+
Some("leader_registry_index_missing")
|
|
4654
|
+
);
|
|
4655
|
+
assert!(!wrong_socket.0);
|
|
4656
|
+
assert_eq!(
|
|
4657
|
+
wrong_socket.1.as_deref(),
|
|
4658
|
+
Some("leader_registry_index_missing")
|
|
4659
|
+
);
|
|
4660
|
+
assert!(!wrong_uuid.0);
|
|
4661
|
+
assert_eq!(
|
|
4662
|
+
wrong_uuid.1.as_deref(),
|
|
4663
|
+
Some("leader_registry_index_missing")
|
|
4664
|
+
);
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
#[test]
|
|
4668
|
+
#[serial_test::serial(env)]
|
|
4669
|
+
fn maintain_live_binding_index_register_failure_is_public() {
|
|
4670
|
+
let root = std::env::temp_dir().join(format!(
|
|
4671
|
+
"ta-n1-debt-{}-{}",
|
|
4672
|
+
std::process::id(),
|
|
4673
|
+
std::time::SystemTime::now()
|
|
4674
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4675
|
+
.unwrap()
|
|
4676
|
+
.as_nanos()
|
|
4677
|
+
));
|
|
4678
|
+
let home = root.join("home");
|
|
4679
|
+
let workspace = root.join("ws");
|
|
4680
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
4681
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
4682
|
+
std::fs::write(home.join(".team-agent"), b"not-a-directory").unwrap();
|
|
4683
|
+
let previous = std::env::var_os("HOME");
|
|
4684
|
+
std::env::set_var("HOME", &home);
|
|
4685
|
+
let state = serde_json::json!({
|
|
4686
|
+
"teams": {
|
|
4687
|
+
"alpha": {
|
|
4688
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 1},
|
|
4689
|
+
"leader_receiver": {
|
|
4690
|
+
"status": "attached",
|
|
4691
|
+
"pane_id": "%1",
|
|
4692
|
+
"owner_epoch": 1,
|
|
4693
|
+
"tmux_socket": "/tmp/ta-n1-debt.sock"
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
});
|
|
4698
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
4699
|
+
let debt = maintain_live_binding_index(&workspace, Some("alpha"), &state);
|
|
4700
|
+
let (ok, reason) = apply_binding_maintain_failures(&debt, true, None);
|
|
4701
|
+
let (keep_ok, keep_reason) = apply_binding_maintain_failures(
|
|
4702
|
+
&debt,
|
|
4703
|
+
false,
|
|
4704
|
+
Some("leader_registry_index_missing".to_string()),
|
|
4705
|
+
);
|
|
4706
|
+
let coordinator = crate::lifecycle::CoordinatorStartSummary {
|
|
4707
|
+
ok: true,
|
|
4708
|
+
status: "started".to_string(),
|
|
4709
|
+
pid: None,
|
|
4710
|
+
binary_path: None,
|
|
4711
|
+
binary_version: None,
|
|
4712
|
+
rotation_reason: None,
|
|
4713
|
+
binary_identity_relation: "same".to_string(),
|
|
4714
|
+
};
|
|
4715
|
+
let restarted = crate::cli::lifecycle_port::restart_value(
|
|
4716
|
+
crate::lifecycle::RestartReport::Restarted {
|
|
4717
|
+
session_name: crate::transport::SessionName::new("s"),
|
|
4718
|
+
agents: Vec::new(),
|
|
4719
|
+
coordinator_started: true,
|
|
4720
|
+
coordinator: coordinator.clone(),
|
|
4721
|
+
next_actions: Vec::new(),
|
|
4722
|
+
attach_commands: Vec::new(),
|
|
4723
|
+
attach_window_failures: debt.clone(),
|
|
4724
|
+
leader_bind_ok: ok,
|
|
4725
|
+
leader_bind_reason: reason.clone(),
|
|
4726
|
+
},
|
|
4727
|
+
Some("alpha"),
|
|
4728
|
+
);
|
|
4729
|
+
let partial = crate::cli::lifecycle_port::restart_value(
|
|
4730
|
+
crate::lifecycle::RestartReport::Partial {
|
|
4731
|
+
session_name: crate::transport::SessionName::new("s"),
|
|
4732
|
+
agents: Vec::new(),
|
|
4733
|
+
failed_agents: Vec::new(),
|
|
4734
|
+
coordinator_started: true,
|
|
4735
|
+
coordinator,
|
|
4736
|
+
next_actions: Vec::new(),
|
|
4737
|
+
attach_commands: Vec::new(),
|
|
4738
|
+
attach_window_failures: debt.clone(),
|
|
4739
|
+
leader_bind_ok: ok,
|
|
4740
|
+
leader_bind_reason: reason.clone(),
|
|
4741
|
+
},
|
|
4742
|
+
Some("alpha"),
|
|
4743
|
+
);
|
|
4744
|
+
match previous {
|
|
4745
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
4746
|
+
None => std::env::remove_var("HOME"),
|
|
4747
|
+
}
|
|
4748
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
4749
|
+
assert_eq!(
|
|
4750
|
+
debt.as_ref()
|
|
4751
|
+
.and_then(|value| value.get("registry_maintain_failed"))
|
|
4752
|
+
.and_then(serde_json::Value::as_bool),
|
|
4753
|
+
Some(true)
|
|
4754
|
+
);
|
|
4755
|
+
assert!(!ok);
|
|
4756
|
+
assert_eq!(reason.as_deref(), Some("leader_registry_maintain_failed"));
|
|
4757
|
+
assert!(!keep_ok);
|
|
4758
|
+
assert_eq!(
|
|
4759
|
+
keep_reason.as_deref(),
|
|
4760
|
+
Some("leader_registry_index_missing"),
|
|
4761
|
+
"earlier bind reason must not be overwritten by maintain failure"
|
|
4762
|
+
);
|
|
4763
|
+
assert_eq!(restarted.get("ok").and_then(serde_json::Value::as_bool), Some(false));
|
|
4764
|
+
assert_eq!(
|
|
4765
|
+
restarted.get("reason").and_then(serde_json::Value::as_str),
|
|
4766
|
+
Some("leader_registry_maintain_failed")
|
|
4767
|
+
);
|
|
4768
|
+
assert_eq!(
|
|
4769
|
+
partial.get("leader_bind_ok").and_then(serde_json::Value::as_bool),
|
|
4770
|
+
Some(false)
|
|
4771
|
+
);
|
|
4772
|
+
assert_eq!(
|
|
4773
|
+
partial
|
|
4774
|
+
.get("leader_bind_reason")
|
|
4775
|
+
.and_then(serde_json::Value::as_str),
|
|
4776
|
+
Some("leader_registry_maintain_failed")
|
|
4777
|
+
);
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
#[test]
|
|
4781
|
+
#[serial_test::serial(env)]
|
|
4782
|
+
fn restart_readiness_accepts_only_healthy_or_newer_daemon() {
|
|
4783
|
+
let identity = crate::coordinator::current_coordinator_binary_identity();
|
|
4784
|
+
let cases = vec![
|
|
4785
|
+
(
|
|
4786
|
+
"same",
|
|
4787
|
+
std::process::id(),
|
|
4788
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4789
|
+
true,
|
|
4790
|
+
identity.binary_version.clone(),
|
|
4791
|
+
identity.binary_path.clone(),
|
|
4792
|
+
true,
|
|
4793
|
+
),
|
|
4794
|
+
(
|
|
4795
|
+
"newer",
|
|
4796
|
+
std::process::id(),
|
|
4797
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4798
|
+
true,
|
|
4799
|
+
"999.0.0".to_string(),
|
|
4800
|
+
identity.binary_path.clone(),
|
|
4801
|
+
true,
|
|
4802
|
+
),
|
|
4803
|
+
(
|
|
4804
|
+
"caller-newer",
|
|
4805
|
+
std::process::id(),
|
|
4806
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4807
|
+
true,
|
|
4808
|
+
"0.0.1".to_string(),
|
|
4809
|
+
identity.binary_path.clone(),
|
|
4810
|
+
false,
|
|
4811
|
+
),
|
|
4812
|
+
(
|
|
4813
|
+
"same-version-path-mismatch",
|
|
4814
|
+
std::process::id(),
|
|
4815
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4816
|
+
true,
|
|
4817
|
+
identity.binary_version.clone(),
|
|
4818
|
+
"/tmp/team-agent-other-binary".to_string(),
|
|
4819
|
+
false,
|
|
4820
|
+
),
|
|
4821
|
+
(
|
|
4822
|
+
"unknown-identity",
|
|
4823
|
+
std::process::id(),
|
|
4824
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4825
|
+
true,
|
|
4826
|
+
"not-a-version".to_string(),
|
|
4827
|
+
identity.binary_path.clone(),
|
|
4828
|
+
false,
|
|
4829
|
+
),
|
|
4830
|
+
(
|
|
4831
|
+
"dead-process",
|
|
4832
|
+
u32::MAX,
|
|
4833
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4834
|
+
true,
|
|
4835
|
+
"999.0.0".to_string(),
|
|
4836
|
+
identity.binary_path.clone(),
|
|
4837
|
+
false,
|
|
4838
|
+
),
|
|
4839
|
+
(
|
|
4840
|
+
"protocol-mismatch",
|
|
4841
|
+
std::process::id(),
|
|
4842
|
+
crate::coordinator::PROTOCOL_VERSION + 1,
|
|
4843
|
+
true,
|
|
4844
|
+
"999.0.0".to_string(),
|
|
4845
|
+
identity.binary_path.clone(),
|
|
4846
|
+
false,
|
|
4847
|
+
),
|
|
4848
|
+
(
|
|
4849
|
+
"schema-mismatch",
|
|
4850
|
+
std::process::id(),
|
|
4851
|
+
crate::coordinator::PROTOCOL_VERSION,
|
|
4852
|
+
false,
|
|
4853
|
+
"999.0.0".to_string(),
|
|
4854
|
+
identity.binary_path.clone(),
|
|
4855
|
+
false,
|
|
4856
|
+
),
|
|
4857
|
+
];
|
|
4858
|
+
|
|
4859
|
+
for (tag, pid, protocol, schema_ok, version, path, expected) in cases {
|
|
4860
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4861
|
+
"ta-restart-readiness-002-{tag}-{}",
|
|
4862
|
+
std::process::id()
|
|
4863
|
+
));
|
|
4864
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
4865
|
+
seed_restart_readiness_health(&workspace, pid, protocol, schema_ok, &version, &path);
|
|
4866
|
+
let transport = crate::transport::test_support::OfflineTransport::new()
|
|
4867
|
+
.with_session_present(true);
|
|
4868
|
+
let readiness = restart_readiness(
|
|
4869
|
+
&workspace,
|
|
4870
|
+
&serde_json::json!({}),
|
|
4871
|
+
&SessionName::new("team-current"),
|
|
4872
|
+
&[],
|
|
4873
|
+
&transport,
|
|
4874
|
+
);
|
|
4875
|
+
assert_eq!(
|
|
4876
|
+
readiness.coordinator_alive, expected,
|
|
4877
|
+
"{tag}: coordinator readiness mismatch; readiness={readiness:?}"
|
|
4878
|
+
);
|
|
4879
|
+
assert!(readiness.session_created, "{tag}: session gate changed");
|
|
4880
|
+
assert!(readiness.worker_pane_addressable, "{tag}: pane gate changed");
|
|
4881
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
#[test]
|
|
4886
|
+
fn restart_readiness_timeout_message_preserves_workspace_diagnosis() {
|
|
4887
|
+
let workspace = Path::new("/tmp/team-agent-restart-002-workspace");
|
|
4888
|
+
let message = restart_readiness_timeout_message(
|
|
4889
|
+
workspace,
|
|
4890
|
+
RestartReadiness {
|
|
4891
|
+
session_created: true,
|
|
4892
|
+
worker_pane_addressable: true,
|
|
4893
|
+
coordinator_alive: false,
|
|
4894
|
+
},
|
|
4895
|
+
std::time::Duration::from_secs(30),
|
|
4896
|
+
);
|
|
4897
|
+
assert!(message.contains("team-agent diagnose --workspace"));
|
|
4898
|
+
assert!(message.contains(workspace.to_string_lossy().as_ref()));
|
|
4899
|
+
assert!(!message.contains("--allow-fresh"));
|
|
4900
|
+
assert!(!message.contains("restart <agent>"));
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
fn seed_restart_readiness_health(
|
|
4904
|
+
workspace: &Path,
|
|
4905
|
+
pid: u32,
|
|
4906
|
+
protocol: u32,
|
|
4907
|
+
schema_ok: bool,
|
|
4908
|
+
binary_version: &str,
|
|
4909
|
+
binary_path: &str,
|
|
4910
|
+
) {
|
|
4911
|
+
let runtime = crate::model::paths::runtime_dir(workspace);
|
|
4912
|
+
std::fs::create_dir_all(&runtime).expect("create readiness runtime");
|
|
4913
|
+
if schema_ok {
|
|
4914
|
+
crate::message_store::MessageStore::open(workspace)
|
|
4915
|
+
.expect("create readiness message store");
|
|
4916
|
+
} else {
|
|
4917
|
+
std::fs::create_dir(runtime.join("team.db")).expect("corrupt readiness schema");
|
|
4918
|
+
}
|
|
4919
|
+
let wp = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
4920
|
+
let metadata = serde_json::json!({
|
|
4921
|
+
"pid": pid,
|
|
4922
|
+
"protocol_version": protocol,
|
|
4923
|
+
"message_store_schema_version": crate::db::schema::SCHEMA_VERSION,
|
|
4924
|
+
"binary_path": binary_path,
|
|
4925
|
+
"binary_version": binary_version,
|
|
4926
|
+
"source": "boot",
|
|
4927
|
+
"updated_at": "2026-09-10T00:00:00Z",
|
|
4928
|
+
});
|
|
4929
|
+
std::fs::write(
|
|
4930
|
+
crate::coordinator::coordinator_meta_path(&wp),
|
|
4931
|
+
serde_json::to_vec_pretty(&metadata).expect("serialize readiness metadata"),
|
|
4932
|
+
)
|
|
4933
|
+
.expect("write readiness metadata");
|
|
4934
|
+
std::fs::write(
|
|
4935
|
+
crate::coordinator::coordinator_pid_path(&wp),
|
|
4936
|
+
pid.to_string(),
|
|
4937
|
+
)
|
|
4938
|
+
.expect("write readiness pid");
|
|
4939
|
+
}
|
|
4010
4940
|
}
|