@team-agent/installer 0.5.51 → 0.5.52
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/named_address.rs +35 -67
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -1
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +19 -3
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +100 -1
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +331 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +12 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -939,103 +939,71 @@ fn resolve_leader(
|
|
|
939
939
|
);
|
|
940
940
|
return Err(err);
|
|
941
941
|
}
|
|
942
|
-
if crate::codex_app_server::receiver_is_app_server(receiver) {
|
|
943
|
-
return resolve_app_server_leader(
|
|
944
|
-
sender_workspace,
|
|
945
|
-
target_workspace,
|
|
946
|
-
team,
|
|
947
|
-
parsed,
|
|
948
|
-
receiver,
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
let pane_id = string_field(receiver, "pane_id")
|
|
952
|
-
.filter(|pane| !pane.is_empty())
|
|
953
|
-
.ok_or_else(|| {
|
|
954
|
-
let mut err = leader_not_attached_third_party(
|
|
955
|
-
target_workspace,
|
|
956
|
-
team,
|
|
957
|
-
None,
|
|
958
|
-
None,
|
|
959
|
-
None,
|
|
960
|
-
transport.tmux_endpoint(),
|
|
961
|
-
);
|
|
962
|
-
err.candidates =
|
|
963
|
-
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
964
|
-
err
|
|
965
|
-
})?;
|
|
966
942
|
let session = string_field(receiver, "session_name");
|
|
967
943
|
let window = string_field(receiver, "window_name");
|
|
968
944
|
let socket = string_field(receiver, "tmux_socket")
|
|
969
945
|
.map(str::to_string)
|
|
970
946
|
.or_else(|| transport.tmux_endpoint());
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
.window_name
|
|
980
|
-
.as_ref()
|
|
981
|
-
.is_some_and(|name| name.as_str() == expected)
|
|
982
|
-
})
|
|
983
|
-
})
|
|
984
|
-
.collect::<Vec<_>>();
|
|
985
|
-
let stale_pane_seen = targets
|
|
986
|
-
.iter()
|
|
987
|
-
.any(|target| target.pane_id.as_str() == pane_id);
|
|
988
|
-
match matches.len() {
|
|
989
|
-
1 => Ok(ResolvedNamedAddress {
|
|
947
|
+
match crate::messaging::resolve_live_leader_channel(
|
|
948
|
+
target_workspace,
|
|
949
|
+
receiver,
|
|
950
|
+
transport,
|
|
951
|
+
) {
|
|
952
|
+
crate::messaging::LeaderChannelResolution::Live(
|
|
953
|
+
crate::messaging::LiveLeaderChannel::DirectTmux(channel),
|
|
954
|
+
) => Ok(ResolvedNamedAddress {
|
|
990
955
|
raw_name: parsed.display_name(),
|
|
991
956
|
target_kind: NamedTargetKind::Leader,
|
|
992
957
|
sender_workspace: sender_workspace.to_path_buf(),
|
|
993
958
|
target_workspace: target_workspace.to_path_buf(),
|
|
994
959
|
team_key: Some(team.to_string()),
|
|
995
960
|
agent_id: None,
|
|
996
|
-
pane_id: pane_id.
|
|
961
|
+
pane_id: channel.pane_id.clone(),
|
|
997
962
|
session_name: session.map(str::to_string),
|
|
998
963
|
window_name: window.map(str::to_string),
|
|
999
964
|
tmux_endpoint: socket,
|
|
1000
965
|
transport_kind: Some("direct_tmux".to_string()),
|
|
1001
966
|
app_server: None,
|
|
1002
|
-
state_pane_id: Some(pane_id
|
|
967
|
+
state_pane_id: Some(channel.pane_id),
|
|
1003
968
|
state_pane_stale: false,
|
|
1004
969
|
agent_status: None,
|
|
1005
|
-
warning:
|
|
970
|
+
warning: (!channel.metadata_drift.is_empty()).then(|| {
|
|
971
|
+
format!(
|
|
972
|
+
"leader_channel_metadata_drift:{}",
|
|
973
|
+
channel.metadata_drift.join(",")
|
|
974
|
+
)
|
|
975
|
+
}),
|
|
1006
976
|
}),
|
|
1007
|
-
|
|
977
|
+
crate::messaging::LeaderChannelResolution::Live(
|
|
978
|
+
crate::messaging::LiveLeaderChannel::CodexAppServer(_),
|
|
979
|
+
) => resolve_app_server_leader(sender_workspace, target_workspace, team, parsed, receiver),
|
|
980
|
+
crate::messaging::LeaderChannelResolution::Unbound(reason) => {
|
|
981
|
+
let pane_id = string_field(receiver, "pane_id");
|
|
1008
982
|
let mut err = leader_not_attached_third_party(
|
|
1009
983
|
target_workspace,
|
|
1010
984
|
team,
|
|
1011
|
-
|
|
985
|
+
pane_id,
|
|
1012
986
|
session,
|
|
1013
987
|
window,
|
|
1014
988
|
socket,
|
|
1015
989
|
);
|
|
1016
|
-
|
|
1017
|
-
err.log = format!("{} stale_tuple_mismatch=true", err.log);
|
|
1018
|
-
}
|
|
990
|
+
err.log = format!("{} channel_unbound={reason:?}", err.log);
|
|
1019
991
|
err.candidates =
|
|
1020
992
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
1021
993
|
Err(err)
|
|
1022
994
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
})
|
|
1036
|
-
})
|
|
1037
|
-
.collect(),
|
|
1038
|
-
)),
|
|
995
|
+
crate::messaging::LeaderChannelResolution::ProbeFailed(error) => {
|
|
996
|
+
let mut err = leader_not_attached_third_party(
|
|
997
|
+
target_workspace,
|
|
998
|
+
team,
|
|
999
|
+
string_field(receiver, "pane_id"),
|
|
1000
|
+
session,
|
|
1001
|
+
window,
|
|
1002
|
+
socket,
|
|
1003
|
+
);
|
|
1004
|
+
err.log = format!("{} channel_probe_failed={error}", err.log);
|
|
1005
|
+
Err(err)
|
|
1006
|
+
}
|
|
1039
1007
|
}
|
|
1040
1008
|
}
|
|
1041
1009
|
|
|
@@ -400,6 +400,7 @@ fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
|
|
|
400
400
|
crate::state::persist::save_runtime_state(
|
|
401
401
|
&ws,
|
|
402
402
|
&json!({
|
|
403
|
+
"active_team_key": "wait-ready",
|
|
403
404
|
"agents": {"w1": {"status": "starting"}},
|
|
404
405
|
"tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
|
|
405
406
|
"leader_receiver": {"status": "attached"},
|
|
@@ -712,25 +712,46 @@ fn claim_lease_no_incident_with_target(
|
|
|
712
712
|
candidate_source,
|
|
713
713
|
pre_epoch,
|
|
714
714
|
);
|
|
715
|
-
|
|
715
|
+
let observation_refreshed = caller_pane_info.is_some_and(|pane_info| {
|
|
716
|
+
refresh_already_bound_receiver_observation(
|
|
717
|
+
state,
|
|
718
|
+
pane_info,
|
|
719
|
+
observed_endpoint.or(current_endpoint.as_deref()),
|
|
720
|
+
scoped_team.or(team),
|
|
721
|
+
)
|
|
722
|
+
});
|
|
723
|
+
if converged || observation_refreshed {
|
|
716
724
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
{
|
|
729
|
-
|
|
725
|
+
if observation_refreshed {
|
|
726
|
+
event_log.write(
|
|
727
|
+
"leader_receiver.observation_refreshed",
|
|
728
|
+
json!({
|
|
729
|
+
"pane_id": caller_pane.as_str(),
|
|
730
|
+
"owner_epoch": pre_epoch.0,
|
|
731
|
+
"owner_changed": false,
|
|
732
|
+
"source": "claim_leader_already_bound",
|
|
733
|
+
}),
|
|
734
|
+
)?;
|
|
735
|
+
}
|
|
736
|
+
if converged {
|
|
737
|
+
topology_convergence = verify_persisted_topology_convergence(
|
|
738
|
+
workspace,
|
|
739
|
+
team_id.as_str(),
|
|
730
740
|
topology_convergence,
|
|
731
741
|
pre_epoch,
|
|
732
|
-
|
|
733
|
-
|
|
742
|
+
)?;
|
|
743
|
+
if topology_convergence
|
|
744
|
+
.as_ref()
|
|
745
|
+
.and_then(|metadata| metadata.get("status"))
|
|
746
|
+
.and_then(Value::as_str)
|
|
747
|
+
!= Some("converged")
|
|
748
|
+
{
|
|
749
|
+
return Ok(convergence_persistence_refusal(
|
|
750
|
+
topology_convergence,
|
|
751
|
+
pre_epoch,
|
|
752
|
+
Some(caller_pane.clone()),
|
|
753
|
+
));
|
|
754
|
+
}
|
|
734
755
|
}
|
|
735
756
|
}
|
|
736
757
|
return Ok(LeaseResult {
|
|
@@ -1659,6 +1680,108 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
|
|
|
1659
1680
|
)
|
|
1660
1681
|
}
|
|
1661
1682
|
|
|
1683
|
+
fn refresh_already_bound_receiver_observation(
|
|
1684
|
+
state: &mut Value,
|
|
1685
|
+
observed: &PaneInfo,
|
|
1686
|
+
observed_endpoint: Option<&str>,
|
|
1687
|
+
team_key: Option<&str>,
|
|
1688
|
+
) -> bool {
|
|
1689
|
+
let attached_at = now_ts();
|
|
1690
|
+
let mut refreshed = state
|
|
1691
|
+
.get_mut("leader_receiver")
|
|
1692
|
+
.and_then(Value::as_object_mut)
|
|
1693
|
+
.is_some_and(|receiver| {
|
|
1694
|
+
refresh_receiver_observation(
|
|
1695
|
+
receiver,
|
|
1696
|
+
observed,
|
|
1697
|
+
observed_endpoint,
|
|
1698
|
+
attached_at.as_str(),
|
|
1699
|
+
)
|
|
1700
|
+
});
|
|
1701
|
+
if let Some(team_key) = team_key {
|
|
1702
|
+
refreshed |= state
|
|
1703
|
+
.get_mut("teams")
|
|
1704
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
1705
|
+
.and_then(|entry| entry.get_mut("leader_receiver"))
|
|
1706
|
+
.and_then(Value::as_object_mut)
|
|
1707
|
+
.is_some_and(|receiver| {
|
|
1708
|
+
refresh_receiver_observation(
|
|
1709
|
+
receiver,
|
|
1710
|
+
observed,
|
|
1711
|
+
observed_endpoint,
|
|
1712
|
+
attached_at.as_str(),
|
|
1713
|
+
)
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
refreshed
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
fn refresh_receiver_observation(
|
|
1720
|
+
receiver: &mut serde_json::Map<String, Value>,
|
|
1721
|
+
observed: &PaneInfo,
|
|
1722
|
+
observed_endpoint: Option<&str>,
|
|
1723
|
+
attached_at: &str,
|
|
1724
|
+
) -> bool {
|
|
1725
|
+
let before = Value::Object(receiver.clone());
|
|
1726
|
+
receiver.insert(
|
|
1727
|
+
"session_name".to_string(),
|
|
1728
|
+
Value::String(observed.session.as_str().to_string()),
|
|
1729
|
+
);
|
|
1730
|
+
receiver.insert(
|
|
1731
|
+
"window_index".to_string(),
|
|
1732
|
+
observed
|
|
1733
|
+
.window_index
|
|
1734
|
+
.map(|value| Value::String(value.to_string()))
|
|
1735
|
+
.unwrap_or(Value::Null),
|
|
1736
|
+
);
|
|
1737
|
+
receiver.insert(
|
|
1738
|
+
"window_name".to_string(),
|
|
1739
|
+
observed
|
|
1740
|
+
.window_name
|
|
1741
|
+
.as_ref()
|
|
1742
|
+
.map(|value| Value::String(value.as_str().to_string()))
|
|
1743
|
+
.unwrap_or(Value::Null),
|
|
1744
|
+
);
|
|
1745
|
+
receiver.insert(
|
|
1746
|
+
"pane_index".to_string(),
|
|
1747
|
+
observed
|
|
1748
|
+
.pane_index
|
|
1749
|
+
.map(|value| Value::String(value.to_string()))
|
|
1750
|
+
.unwrap_or(Value::Null),
|
|
1751
|
+
);
|
|
1752
|
+
receiver.insert(
|
|
1753
|
+
"pane_tty".to_string(),
|
|
1754
|
+
observed
|
|
1755
|
+
.tty
|
|
1756
|
+
.as_ref()
|
|
1757
|
+
.map(|value| Value::String(value.clone()))
|
|
1758
|
+
.unwrap_or(Value::Null),
|
|
1759
|
+
);
|
|
1760
|
+
receiver.insert(
|
|
1761
|
+
"pane_current_command".to_string(),
|
|
1762
|
+
observed
|
|
1763
|
+
.current_command
|
|
1764
|
+
.as_ref()
|
|
1765
|
+
.map(|value| Value::String(value.clone()))
|
|
1766
|
+
.unwrap_or(Value::Null),
|
|
1767
|
+
);
|
|
1768
|
+
receiver.insert(
|
|
1769
|
+
"fingerprint".to_string(),
|
|
1770
|
+
Value::String(receiver_fingerprint(observed)),
|
|
1771
|
+
);
|
|
1772
|
+
if let Some(endpoint) = observed_endpoint.filter(|value| !value.is_empty()) {
|
|
1773
|
+
receiver.insert(
|
|
1774
|
+
"tmux_socket".to_string(),
|
|
1775
|
+
Value::String(endpoint.to_string()),
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
receiver.insert(
|
|
1779
|
+
"attached_at".to_string(),
|
|
1780
|
+
Value::String(attached_at.to_string()),
|
|
1781
|
+
);
|
|
1782
|
+
Value::Object(receiver.clone()) != before
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1662
1785
|
fn make_owner(
|
|
1663
1786
|
provider: Provider,
|
|
1664
1787
|
pane: &NonEmptyPaneId,
|
|
@@ -2124,6 +2247,22 @@ fn readable_team_snapshot_path(w: &BP, n: &str) -> BF /* B0_DIAGNOSTIC_LEGACY_SN
|
|
|
2124
2247
|
mod tests {
|
|
2125
2248
|
use super::*;
|
|
2126
2249
|
|
|
2250
|
+
fn observed_pane() -> PaneInfo {
|
|
2251
|
+
PaneInfo {
|
|
2252
|
+
pane_id: PaneId::new("%7"),
|
|
2253
|
+
session: crate::transport::SessionName::new("new-session"),
|
|
2254
|
+
window_index: Some(3),
|
|
2255
|
+
window_name: Some(crate::transport::WindowName::new("new-window")),
|
|
2256
|
+
pane_index: Some(1),
|
|
2257
|
+
tty: Some("/dev/ttys007".to_string()),
|
|
2258
|
+
current_command: Some("codex".to_string()),
|
|
2259
|
+
current_path: None,
|
|
2260
|
+
active: true,
|
|
2261
|
+
pane_pid: Some(700),
|
|
2262
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2127
2266
|
#[test]
|
|
2128
2267
|
fn attribute_command_provider_recognizes_copilot() {
|
|
2129
2268
|
assert_eq!(
|
|
@@ -2135,4 +2274,51 @@ mod tests {
|
|
|
2135
2274
|
Some(Provider::Copilot)
|
|
2136
2275
|
);
|
|
2137
2276
|
}
|
|
2277
|
+
|
|
2278
|
+
#[test]
|
|
2279
|
+
fn already_bound_refresh_updates_observations_without_reclaiming() {
|
|
2280
|
+
let mut state = json!({
|
|
2281
|
+
"leader_receiver": {
|
|
2282
|
+
"status": "attached",
|
|
2283
|
+
"pane_id": "%7",
|
|
2284
|
+
"owner_epoch": 10,
|
|
2285
|
+
"owner_agent_id": "leader",
|
|
2286
|
+
"session_name": "old-session",
|
|
2287
|
+
"pane_tty": "/dev/old",
|
|
2288
|
+
"attached_at": "2026-01-01T00:00:00Z"
|
|
2289
|
+
},
|
|
2290
|
+
"teams": {"team-a": {"leader_receiver": {
|
|
2291
|
+
"status": "attached",
|
|
2292
|
+
"pane_id": "%7",
|
|
2293
|
+
"owner_epoch": 10,
|
|
2294
|
+
"owner_agent_id": "leader",
|
|
2295
|
+
"session_name": "old-session",
|
|
2296
|
+
"pane_tty": "/dev/old",
|
|
2297
|
+
"attached_at": "2026-01-01T00:00:00Z"
|
|
2298
|
+
}}}
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
assert!(refresh_already_bound_receiver_observation(
|
|
2302
|
+
&mut state,
|
|
2303
|
+
&observed_pane(),
|
|
2304
|
+
Some("/tmp/team-agent-live.sock"),
|
|
2305
|
+
Some("team-a"),
|
|
2306
|
+
));
|
|
2307
|
+
let receiver = &state["leader_receiver"];
|
|
2308
|
+
assert_eq!(receiver["owner_epoch"], json!(10));
|
|
2309
|
+
assert_eq!(receiver["owner_agent_id"], json!("leader"));
|
|
2310
|
+
assert_eq!(receiver["session_name"], json!("new-session"));
|
|
2311
|
+
assert_eq!(receiver["window_name"], json!("new-window"));
|
|
2312
|
+
assert_eq!(receiver["pane_tty"], json!("/dev/ttys007"));
|
|
2313
|
+
assert_eq!(receiver["tmux_socket"], json!("/tmp/team-agent-live.sock"));
|
|
2314
|
+
assert_ne!(receiver["attached_at"], json!("2026-01-01T00:00:00Z"));
|
|
2315
|
+
assert_eq!(
|
|
2316
|
+
state["teams"]["team-a"]["leader_receiver"]["session_name"],
|
|
2317
|
+
json!("new-session")
|
|
2318
|
+
);
|
|
2319
|
+
assert_eq!(
|
|
2320
|
+
state["teams"]["team-a"]["leader_receiver"]["owner_epoch"],
|
|
2321
|
+
json!(10)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2138
2324
|
}
|
|
@@ -193,7 +193,13 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
193
193
|
persist_effective_approval_policy(obj, safety);
|
|
194
194
|
}
|
|
195
195
|
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
196
|
-
|
|
196
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
197
|
+
save_launched_team_state_for_key(
|
|
198
|
+
workspace,
|
|
199
|
+
&state,
|
|
200
|
+
Some(canonical_team_key),
|
|
201
|
+
Some(agent_id.as_str()),
|
|
202
|
+
)
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
|
|
@@ -379,7 +379,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
379
379
|
// to "unknown" in the payload) — Phase 2 refactor will thread
|
|
380
380
|
// ResolvedTransport.source through the launch signature.
|
|
381
381
|
annotate_runtime_transport(&mut state, transport, &workspace, None);
|
|
382
|
-
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
382
|
+
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key), None)?;
|
|
383
383
|
annotate_persisted_team_depth(
|
|
384
384
|
&workspace,
|
|
385
385
|
&state_team_key,
|
|
@@ -146,7 +146,7 @@ pub(super) fn persist_spawn_agent_state(
|
|
|
146
146
|
obj.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
147
147
|
state = serde_json::Value::Object(obj);
|
|
148
148
|
}
|
|
149
|
-
save_launched_team_state_for_key(workspace, &state, Some(&team_id))
|
|
149
|
+
save_launched_team_state_for_key(workspace, &state, Some(&team_id), None)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
pub(super) fn pane_pids_by_started_agent(
|
|
@@ -178,13 +178,14 @@ pub(super) fn save_launched_team_state(
|
|
|
178
178
|
workspace: &Path,
|
|
179
179
|
launched: &serde_json::Value,
|
|
180
180
|
) -> Result<(), LifecycleError> {
|
|
181
|
-
save_launched_team_state_for_key(workspace, launched, None)
|
|
181
|
+
save_launched_team_state_for_key(workspace, launched, None, None)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
pub(super) fn save_launched_team_state_for_key(
|
|
185
185
|
workspace: &Path,
|
|
186
186
|
launched: &serde_json::Value,
|
|
187
187
|
team_key: Option<&str>,
|
|
188
|
+
added_agent_id: Option<&str>,
|
|
188
189
|
) -> Result<(), LifecycleError> {
|
|
189
190
|
let existing = load_runtime_state(workspace).unwrap_or_else(|_| serde_json::json!({}));
|
|
190
191
|
let launched_key = team_key
|
|
@@ -231,13 +232,12 @@ pub(super) fn save_launched_team_state_for_key(
|
|
|
231
232
|
};
|
|
232
233
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
233
234
|
drop_unbound_top_level_owner(&mut projected);
|
|
235
|
+
let intent = crate::state::repository::StateWriteIntent::launch_team_or_add_agent(
|
|
236
|
+
&launched_key,
|
|
237
|
+
added_agent_id,
|
|
238
|
+
);
|
|
234
239
|
crate::state::repository::StateRepository::new(workspace)
|
|
235
|
-
.save(
|
|
236
|
-
crate::state::repository::StateWriteIntent::LaunchTeam {
|
|
237
|
-
team_key: &launched_key,
|
|
238
|
-
},
|
|
239
|
-
&projected,
|
|
240
|
-
)
|
|
240
|
+
.save(intent, &projected)
|
|
241
241
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -18,13 +18,15 @@ pub fn start_agent(
|
|
|
18
18
|
team: Option<&str>,
|
|
19
19
|
) -> Result<StartAgentOutcome, LifecycleError> {
|
|
20
20
|
let paths = lifecycle_paths(workspace, team)?;
|
|
21
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
22
|
+
let team = canonical_team.as_deref();
|
|
21
23
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
22
24
|
workspace: &paths.run_workspace,
|
|
23
25
|
operation: "start-agent",
|
|
24
26
|
team,
|
|
25
27
|
agent_id: Some(agent_id),
|
|
26
28
|
})?;
|
|
27
|
-
let transport =
|
|
29
|
+
let transport = paths.tmux_backend()?;
|
|
28
30
|
start_agent_at_paths(
|
|
29
31
|
&paths.run_workspace,
|
|
30
32
|
&paths.spec_workspace,
|
|
@@ -54,9 +56,12 @@ pub fn start_agent_with_transport(
|
|
|
54
56
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
55
57
|
run_workspace: workspace.to_path_buf(),
|
|
56
58
|
spec_workspace: workspace.to_path_buf(),
|
|
59
|
+
selected: None,
|
|
57
60
|
},
|
|
58
61
|
Err(error) => return Err(error),
|
|
59
62
|
};
|
|
63
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
64
|
+
let team = canonical_team.as_deref();
|
|
60
65
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
61
66
|
workspace: &paths.run_workspace,
|
|
62
67
|
operation: "start-agent",
|
|
@@ -778,13 +783,15 @@ pub fn stop_agent(
|
|
|
778
783
|
team: Option<&str>,
|
|
779
784
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
780
785
|
let paths = lifecycle_paths(workspace, team)?;
|
|
786
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
787
|
+
let team = canonical_team.as_deref();
|
|
781
788
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
782
789
|
workspace: &paths.run_workspace,
|
|
783
790
|
operation: "stop-agent",
|
|
784
791
|
team,
|
|
785
792
|
agent_id: Some(agent_id),
|
|
786
793
|
})?;
|
|
787
|
-
let transport =
|
|
794
|
+
let transport = paths.tmux_backend()?;
|
|
788
795
|
stop_agent_at_paths(
|
|
789
796
|
&paths.run_workspace,
|
|
790
797
|
&paths.spec_workspace,
|
|
@@ -801,6 +808,8 @@ pub fn stop_agent_with_transport(
|
|
|
801
808
|
transport: &dyn crate::transport::Transport,
|
|
802
809
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
803
810
|
let paths = lifecycle_paths(workspace, team)?;
|
|
811
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
812
|
+
let team = canonical_team.as_deref();
|
|
804
813
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
805
814
|
workspace: &paths.run_workspace,
|
|
806
815
|
operation: "stop-agent",
|
|
@@ -840,9 +849,12 @@ pub(crate) fn start_agent_at_paths_for_recovery(
|
|
|
840
849
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
841
850
|
run_workspace: workspace.to_path_buf(),
|
|
842
851
|
spec_workspace: workspace.to_path_buf(),
|
|
852
|
+
selected: None,
|
|
843
853
|
},
|
|
844
854
|
Err(error) => return Err(RecoveryError::Lifecycle(error.to_string())),
|
|
845
855
|
};
|
|
856
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
857
|
+
let team = canonical_team.as_deref();
|
|
846
858
|
// Best-effort stop: if the live pane still exists we tear it down so the
|
|
847
859
|
// subsequent start creates a fresh provider process instead of a Noop.
|
|
848
860
|
// Stop errors are absorbed into the start attempt result — the important
|
|
@@ -1217,13 +1229,15 @@ pub fn reset_agent(
|
|
|
1217
1229
|
});
|
|
1218
1230
|
}
|
|
1219
1231
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1232
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1233
|
+
let team = canonical_team.as_deref();
|
|
1220
1234
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1221
1235
|
workspace: &paths.run_workspace,
|
|
1222
1236
|
operation: "reset-agent",
|
|
1223
1237
|
team,
|
|
1224
1238
|
agent_id: Some(agent_id),
|
|
1225
1239
|
})?;
|
|
1226
|
-
let transport =
|
|
1240
|
+
let transport = paths.tmux_backend()?;
|
|
1227
1241
|
reset_agent_at_paths(
|
|
1228
1242
|
&paths.run_workspace,
|
|
1229
1243
|
&paths.spec_workspace,
|
|
@@ -1249,6 +1263,8 @@ pub fn reset_agent_with_transport(
|
|
|
1249
1263
|
});
|
|
1250
1264
|
}
|
|
1251
1265
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1266
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1267
|
+
let team = canonical_team.as_deref();
|
|
1252
1268
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1253
1269
|
workspace: &paths.run_workspace,
|
|
1254
1270
|
operation: "reset-agent",
|