@team-agent/installer 0.5.43 → 0.5.44
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/mod.rs +99 -9
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +4 -4
- package/crates/team-agent/src/layout/worker_env.rs +35 -0
- package/crates/team-agent/src/leader/lease.rs +40 -16
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- package/crates/team-agent/src/leader/registry.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +49 -14
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/platform/process.rs +33 -2
- package/crates/team-agent/src/state/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/topology.rs +13 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -4100,7 +4100,7 @@ pub mod leader_port {
|
|
|
4100
4100
|
"action": "rerun with --confirm to claim ownership of this team",
|
|
4101
4101
|
}));
|
|
4102
4102
|
}
|
|
4103
|
-
let state = crate::state::persist::
|
|
4103
|
+
let state = crate::state::persist::load_runtime_state_without_migrations(workspace)
|
|
4104
4104
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4105
4105
|
let Some(team_id) = resolve_owner_team_id(&state, team) else {
|
|
4106
4106
|
return Ok(json!({
|
|
@@ -4112,8 +4112,7 @@ pub mod leader_port {
|
|
|
4112
4112
|
}));
|
|
4113
4113
|
};
|
|
4114
4114
|
let requested_team = team;
|
|
4115
|
-
let
|
|
4116
|
-
let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
|
|
4115
|
+
let resolved_team = Some(team_id.as_str().to_string());
|
|
4117
4116
|
if !positive_caller_pane_env_present() {
|
|
4118
4117
|
let bind = crate::leader::bind_owner_from_caller_pane(workspace, &team_id, None)
|
|
4119
4118
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -4124,6 +4123,7 @@ pub mod leader_port {
|
|
|
4124
4123
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4125
4124
|
let mut value = lease_value(result);
|
|
4126
4125
|
insert_resolved_team(&mut value, requested_team, resolved_team.as_deref());
|
|
4126
|
+
append_team_session_ready(workspace, resolved_team.as_deref(), &mut value);
|
|
4127
4127
|
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
4128
4128
|
emit_topology_convergence_event(
|
|
4129
4129
|
workspace,
|
|
@@ -4137,6 +4137,7 @@ pub mod leader_port {
|
|
|
4137
4137
|
"takeover",
|
|
4138
4138
|
&mut value,
|
|
4139
4139
|
);
|
|
4140
|
+
restore_non_target_teams(workspace, &state, resolved_team.as_deref());
|
|
4140
4141
|
}
|
|
4141
4142
|
Ok(value)
|
|
4142
4143
|
}
|
|
@@ -4146,7 +4147,7 @@ pub mod leader_port {
|
|
|
4146
4147
|
team: Option<&str>,
|
|
4147
4148
|
confirm: bool,
|
|
4148
4149
|
) -> Result<Value, CliError> {
|
|
4149
|
-
let state = crate::state::persist::
|
|
4150
|
+
let state = crate::state::persist::load_runtime_state_without_migrations(workspace)
|
|
4150
4151
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4151
4152
|
let Some(team_id) = resolve_owner_team_id(&state, team) else {
|
|
4152
4153
|
return Ok(json!({
|
|
@@ -4165,12 +4166,12 @@ pub mod leader_port {
|
|
|
4165
4166
|
}
|
|
4166
4167
|
return Ok(owner_bind_value(bind));
|
|
4167
4168
|
}
|
|
4168
|
-
let
|
|
4169
|
-
let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
|
|
4169
|
+
let resolved_team = Some(team_id.as_str().to_string());
|
|
4170
4170
|
let result = crate::leader::claim_leader(workspace, resolved_team.as_deref(), confirm)
|
|
4171
4171
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4172
4172
|
let mut value = lease_value(result);
|
|
4173
4173
|
insert_resolved_team(&mut value, team, resolved_team.as_deref());
|
|
4174
|
+
append_team_session_ready(workspace, resolved_team.as_deref(), &mut value);
|
|
4174
4175
|
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
4175
4176
|
emit_topology_convergence_event(
|
|
4176
4177
|
workspace,
|
|
@@ -4184,6 +4185,7 @@ pub mod leader_port {
|
|
|
4184
4185
|
"claim-leader",
|
|
4185
4186
|
&mut value,
|
|
4186
4187
|
);
|
|
4188
|
+
restore_non_target_teams(workspace, &state, resolved_team.as_deref());
|
|
4187
4189
|
}
|
|
4188
4190
|
Ok(value)
|
|
4189
4191
|
}
|
|
@@ -4279,6 +4281,7 @@ pub mod leader_port {
|
|
|
4279
4281
|
"owner_epoch": convergence.get("owner_epoch").cloned().unwrap_or(Value::Null),
|
|
4280
4282
|
"persisted": convergence.get("persisted").cloned().unwrap_or(Value::Bool(false)),
|
|
4281
4283
|
"checked_paths": convergence.get("checked_paths").cloned().unwrap_or_else(|| json!([])),
|
|
4284
|
+
"team_session_ready": response.get("team_session_ready").cloned().unwrap_or(Value::Null),
|
|
4282
4285
|
}),
|
|
4283
4286
|
);
|
|
4284
4287
|
}
|
|
@@ -4344,9 +4347,96 @@ pub mod leader_port {
|
|
|
4344
4347
|
| crate::state::projection::OwnerTeamResolution::Ambiguous { .. } => None,
|
|
4345
4348
|
}
|
|
4346
4349
|
}
|
|
4347
|
-
None => Some(TeamKey::new(
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
+
None => Some(TeamKey::new(active_or_derived_team_key(state))),
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
fn active_or_derived_team_key(state: &Value) -> String {
|
|
4355
|
+
state
|
|
4356
|
+
.get("active_team_key")
|
|
4357
|
+
.and_then(Value::as_str)
|
|
4358
|
+
.filter(|team| !team.is_empty())
|
|
4359
|
+
.map(str::to_string)
|
|
4360
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(state))
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
fn append_team_session_ready(workspace: &Path, team: Option<&str>, response: &mut Value) {
|
|
4364
|
+
let Some(obj) = response.as_object_mut() else {
|
|
4365
|
+
return;
|
|
4366
|
+
};
|
|
4367
|
+
let Some(convergence) = obj.get("topology_convergence") else {
|
|
4368
|
+
return;
|
|
4369
|
+
};
|
|
4370
|
+
if convergence.get("status").and_then(Value::as_str) != Some("converged") {
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
let Some(endpoint) = convergence
|
|
4374
|
+
.get("new_tmux_endpoint")
|
|
4375
|
+
.and_then(Value::as_str)
|
|
4376
|
+
.filter(|endpoint| !endpoint.is_empty())
|
|
4377
|
+
else {
|
|
4378
|
+
obj.insert("team_session_ready".to_string(), Value::Null);
|
|
4379
|
+
return;
|
|
4380
|
+
};
|
|
4381
|
+
let session_name = crate::state::persist::load_runtime_state_without_migrations(workspace)
|
|
4382
|
+
.ok()
|
|
4383
|
+
.and_then(|state| target_team_session_name(&state, team));
|
|
4384
|
+
let ready = session_name
|
|
4385
|
+
.as_deref()
|
|
4386
|
+
.and_then(|session| crate::topology::team_session_ready_on_endpoint(endpoint, session));
|
|
4387
|
+
obj.insert(
|
|
4388
|
+
"team_session_ready".to_string(),
|
|
4389
|
+
ready.map(Value::Bool).unwrap_or(Value::Null),
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
fn target_team_session_name(state: &Value, team: Option<&str>) -> Option<String> {
|
|
4394
|
+
team.and_then(|team| {
|
|
4395
|
+
state
|
|
4396
|
+
.get("teams")
|
|
4397
|
+
.and_then(Value::as_object)
|
|
4398
|
+
.and_then(|teams| teams.get(team))
|
|
4399
|
+
})
|
|
4400
|
+
.and_then(|team_state| team_state.get("session_name"))
|
|
4401
|
+
.and_then(Value::as_str)
|
|
4402
|
+
.filter(|session| !session.is_empty())
|
|
4403
|
+
.or_else(|| state.get("session_name").and_then(Value::as_str))
|
|
4404
|
+
.filter(|session| !session.is_empty())
|
|
4405
|
+
.map(str::to_string)
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
fn restore_non_target_teams(workspace: &Path, before: &Value, target: Option<&str>) {
|
|
4409
|
+
let Some(target) = target.filter(|team| !team.is_empty()) else {
|
|
4410
|
+
return;
|
|
4411
|
+
};
|
|
4412
|
+
let Some(before_teams) = before.get("teams").and_then(Value::as_object) else {
|
|
4413
|
+
return;
|
|
4414
|
+
};
|
|
4415
|
+
let Ok(mut latest) =
|
|
4416
|
+
crate::state::persist::load_runtime_state_without_migrations(workspace)
|
|
4417
|
+
else {
|
|
4418
|
+
return;
|
|
4419
|
+
};
|
|
4420
|
+
let Some(latest_teams) = latest.get_mut("teams").and_then(Value::as_object_mut) else {
|
|
4421
|
+
return;
|
|
4422
|
+
};
|
|
4423
|
+
let mut changed = false;
|
|
4424
|
+
for (team, before_entry) in before_teams {
|
|
4425
|
+
if team == target {
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
if latest_teams.get(team) != Some(before_entry) {
|
|
4429
|
+
latest_teams.insert(team.clone(), before_entry.clone());
|
|
4430
|
+
changed = true;
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
if changed {
|
|
4434
|
+
let _ = crate::state::repository::StateRepository::new(workspace).save(
|
|
4435
|
+
crate::state::repository::StateWriteIntent::LeaderBindingRestoreNonTargetTeams {
|
|
4436
|
+
target_team_key: target,
|
|
4437
|
+
},
|
|
4438
|
+
&latest,
|
|
4439
|
+
);
|
|
4350
4440
|
}
|
|
4351
4441
|
}
|
|
4352
4442
|
|
|
@@ -713,8 +713,8 @@ fn command_process_check_with_marker(
|
|
|
713
713
|
return check;
|
|
714
714
|
}
|
|
715
715
|
process_check(
|
|
716
|
-
ProcessLiveness::
|
|
717
|
-
format!("
|
|
716
|
+
ProcessLiveness::Unverifiable,
|
|
717
|
+
format!("provider_evidence_unverifiable:{command}"),
|
|
718
718
|
)
|
|
719
719
|
}
|
|
720
720
|
|
|
@@ -733,8 +733,8 @@ fn pane_command_process_check_with_marker(
|
|
|
733
733
|
return check;
|
|
734
734
|
}
|
|
735
735
|
process_check(
|
|
736
|
-
ProcessLiveness::
|
|
737
|
-
format!("
|
|
736
|
+
ProcessLiveness::Unverifiable,
|
|
737
|
+
format!("provider_evidence_unverifiable:{command}"),
|
|
738
738
|
)
|
|
739
739
|
}
|
|
740
740
|
|
|
@@ -28,6 +28,7 @@ use std::collections::BTreeMap;
|
|
|
28
28
|
use std::path::{Path, PathBuf};
|
|
29
29
|
|
|
30
30
|
use crate::model::yaml::Value as YamlValue;
|
|
31
|
+
use crate::provider::Provider;
|
|
31
32
|
|
|
32
33
|
/// Env-key PREFIXES that are stripped from the inherited parent env
|
|
33
34
|
/// before worker spawn. These carry leader-process identity and must
|
|
@@ -52,6 +53,10 @@ const STRIP_EXACT: &[&str] = &[
|
|
|
52
53
|
"TMUX_PANE",
|
|
53
54
|
];
|
|
54
55
|
|
|
56
|
+
const WORKER_IDENTITY_EXACT: &[&str] = &["CLAUDECODE", "CLAUDE_EFFORT", "CODEX_THREAD_ID"];
|
|
57
|
+
|
|
58
|
+
const WORKER_IDENTITY_PREFIXES: &[&str] = &["CLAUDE_CODE_"];
|
|
59
|
+
|
|
55
60
|
/// Build the worker spawn env per Python inherit-then-strip semantics.
|
|
56
61
|
///
|
|
57
62
|
/// Inputs:
|
|
@@ -102,6 +107,30 @@ where
|
|
|
102
107
|
env
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
pub(crate) fn isolate_worker_spawn_env(
|
|
111
|
+
_target_provider: Provider,
|
|
112
|
+
env: &mut BTreeMap<String, String>,
|
|
113
|
+
base_env_unset: impl IntoIterator<Item = String>,
|
|
114
|
+
) -> Vec<String> {
|
|
115
|
+
let mut env_unset = base_env_unset
|
|
116
|
+
.into_iter()
|
|
117
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
118
|
+
for key in WORKER_IDENTITY_EXACT {
|
|
119
|
+
env.remove(*key);
|
|
120
|
+
env_unset.insert((*key).to_string());
|
|
121
|
+
}
|
|
122
|
+
let dynamic_keys = env
|
|
123
|
+
.keys()
|
|
124
|
+
.filter(|key| is_worker_identity_key(key))
|
|
125
|
+
.cloned()
|
|
126
|
+
.collect::<Vec<_>>();
|
|
127
|
+
for key in dynamic_keys {
|
|
128
|
+
env.remove(&key);
|
|
129
|
+
env_unset.insert(key);
|
|
130
|
+
}
|
|
131
|
+
env_unset.into_iter().collect()
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
fn is_stripped(key: &str) -> bool {
|
|
106
135
|
if STRIP_EXACT.iter().any(|exact| *exact == key) {
|
|
107
136
|
return true;
|
|
@@ -112,6 +141,12 @@ fn is_stripped(key: &str) -> bool {
|
|
|
112
141
|
false
|
|
113
142
|
}
|
|
114
143
|
|
|
144
|
+
fn is_worker_identity_key(key: &str) -> bool {
|
|
145
|
+
WORKER_IDENTITY_PREFIXES
|
|
146
|
+
.iter()
|
|
147
|
+
.any(|prefix| key.starts_with(prefix))
|
|
148
|
+
}
|
|
149
|
+
|
|
115
150
|
fn is_posix_shell_identifier(s: &str) -> bool {
|
|
116
151
|
let mut bytes = s.bytes();
|
|
117
152
|
let Some(first) = bytes.next() else {
|
|
@@ -487,7 +487,7 @@ pub fn claim_leader(
|
|
|
487
487
|
.filter(|pane| !pane.is_empty())
|
|
488
488
|
})
|
|
489
489
|
.unwrap_or_default();
|
|
490
|
-
let raw_state = crate::state::persist::
|
|
490
|
+
let raw_state = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
|
|
491
491
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
492
492
|
let targets = claim_leader_targets(workspace, &raw_state);
|
|
493
493
|
let caller_candidate = targets
|
|
@@ -505,19 +505,36 @@ pub fn claim_leader(
|
|
|
505
505
|
.ok()
|
|
506
506
|
.filter(|team| !team.is_empty());
|
|
507
507
|
let explicit_team = team.filter(|team| !team.is_empty());
|
|
508
|
+
let active_team = crate::messaging::leader_receiver::active_team_key(workspace, &raw_state);
|
|
509
|
+
let active_team_from_state = raw_state
|
|
510
|
+
.get("active_team_key")
|
|
511
|
+
.and_then(Value::as_str)
|
|
512
|
+
.filter(|team| !team.is_empty());
|
|
508
513
|
let requested_team = explicit_team
|
|
509
|
-
.
|
|
514
|
+
.or(active_team_from_state)
|
|
510
515
|
.or_else(|| {
|
|
511
516
|
caller_target
|
|
512
517
|
.as_ref()
|
|
513
518
|
.and_then(|target| target.team_id.as_deref())
|
|
514
519
|
})
|
|
515
|
-
.or(env_team.as_deref())
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
.or(env_team.as_deref())
|
|
521
|
+
.or(Some(active_team.as_str()));
|
|
522
|
+
let team_id = TeamKey::new(
|
|
523
|
+
requested_team
|
|
524
|
+
.map(str::to_string)
|
|
525
|
+
.unwrap_or_else(|| active_team.clone()),
|
|
526
|
+
);
|
|
527
|
+
let scoped_team = if team_id.as_str() == active_team
|
|
528
|
+
|| raw_state
|
|
529
|
+
.get("teams")
|
|
530
|
+
.and_then(|teams| teams.get(team_id.as_str()))
|
|
531
|
+
.is_some()
|
|
532
|
+
{
|
|
533
|
+
Some(team_id.as_str())
|
|
534
|
+
} else {
|
|
535
|
+
None
|
|
536
|
+
};
|
|
537
|
+
let scoped_team = scoped_team.filter(|team| {
|
|
521
538
|
*team == active_team
|
|
522
539
|
|| raw_state
|
|
523
540
|
.get("teams")
|
|
@@ -1547,10 +1564,11 @@ fn locked_runtime_state(
|
|
|
1547
1564
|
if !path.exists() {
|
|
1548
1565
|
return Ok(None);
|
|
1549
1566
|
}
|
|
1567
|
+
let raw = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
|
|
1550
1568
|
let state = if let Some(team) = scoped_team {
|
|
1551
|
-
crate::state::projection::
|
|
1569
|
+
crate::state::projection::project_top_level_view(&raw, team)
|
|
1552
1570
|
} else {
|
|
1553
|
-
|
|
1571
|
+
raw
|
|
1554
1572
|
};
|
|
1555
1573
|
Ok(Some(state))
|
|
1556
1574
|
}
|
|
@@ -1874,7 +1892,7 @@ fn save_claim_team_scoped_state(
|
|
|
1874
1892
|
state: &Value,
|
|
1875
1893
|
target_key: &str,
|
|
1876
1894
|
) -> Result<(), LeaderError> {
|
|
1877
|
-
let existing = crate::state::persist::
|
|
1895
|
+
let existing = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
|
|
1878
1896
|
let mut teams = existing
|
|
1879
1897
|
.get("teams")
|
|
1880
1898
|
.and_then(Value::as_object)
|
|
@@ -2010,10 +2028,14 @@ fn compact_team_state_preserving_claim_fields(state: &Value, target_key: &str) -
|
|
|
2010
2028
|
/// drifted. Every touch of the legacy path constants below is marked
|
|
2011
2029
|
/// `B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ` so the RED3 grep guard admits
|
|
2012
2030
|
/// them as documented exceptions.
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2031
|
+
type BP = Path;
|
|
2032
|
+
type BJ = Value;
|
|
2033
|
+
type BF = PathBuf;
|
|
2034
|
+
type B0 = Result<Option<Value>, LeaderError>;
|
|
2035
|
+
|
|
2036
|
+
pub fn detect_dual_state_divergence(w: &BP, s: &BJ) -> B0 /* B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ */ {
|
|
2037
|
+
let workspace = w;
|
|
2038
|
+
let state = s;
|
|
2017
2039
|
let Some(session_name) = state.get("session_name").and_then(Value::as_str) else {
|
|
2018
2040
|
return Ok(None);
|
|
2019
2041
|
};
|
|
@@ -2083,7 +2105,9 @@ fn agent_binding_summary(state: &Value) -> Value {
|
|
|
2083
2105
|
Value::Object(out)
|
|
2084
2106
|
}
|
|
2085
2107
|
|
|
2086
|
-
fn readable_team_snapshot_path(
|
|
2108
|
+
fn readable_team_snapshot_path(w: &BP, n: &str) -> BF /* B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ */ {
|
|
2109
|
+
let workspace = w;
|
|
2110
|
+
let session_name = n;
|
|
2087
2111
|
let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name); // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: reuses helpers safe legacy path.
|
|
2088
2112
|
if safe_path.exists() {
|
|
2089
2113
|
return safe_path;
|
|
@@ -23,19 +23,27 @@ pub(crate) fn attribute_pane_provider(pane: &PaneInfo) -> Option<Provider> {
|
|
|
23
23
|
attribute_pane_provider_with_process(pane, provider_from_pid_env, provider_from_pid_argv)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
fn attribute_pane_provider_with_process(
|
|
26
|
+
fn attribute_pane_provider_with_process<FEnv, FArg>(
|
|
27
27
|
pane: &PaneInfo,
|
|
28
|
-
provider_from_env_pid:
|
|
29
|
-
provider_from_argv_pid:
|
|
30
|
-
) -> Option<Provider>
|
|
28
|
+
provider_from_env_pid: FEnv,
|
|
29
|
+
provider_from_argv_pid: FArg,
|
|
30
|
+
) -> Option<Provider>
|
|
31
|
+
where
|
|
32
|
+
FEnv: Fn(u32) -> Option<Provider>,
|
|
33
|
+
FArg: Fn(u32) -> Option<Provider>,
|
|
34
|
+
{
|
|
31
35
|
provider_from_env(&pane.leader_env)
|
|
32
|
-
.or_else(|| pane.pane_pid.and_then(provider_from_env_pid))
|
|
36
|
+
.or_else(|| pane.pane_pid.and_then(|pid| provider_from_env_pid(pid)))
|
|
33
37
|
.or_else(|| {
|
|
34
38
|
pane.current_command
|
|
35
39
|
.as_deref()
|
|
36
40
|
.and_then(attribute_command_provider)
|
|
37
41
|
})
|
|
38
|
-
.or_else(|| pane.pane_pid.and_then(provider_from_argv_pid))
|
|
42
|
+
.or_else(|| pane.pane_pid.and_then(|pid| provider_from_argv_pid(pid)))
|
|
43
|
+
.or_else(|| {
|
|
44
|
+
pane.pane_pid
|
|
45
|
+
.and_then(|pid| provider_from_process_tree_argv(pid, &provider_from_argv_pid))
|
|
46
|
+
})
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
pub(crate) fn attribute_command_provider(command: &str) -> Option<Provider> {
|
|
@@ -63,6 +71,17 @@ fn provider_from_pid_argv(pid: u32) -> Option<Provider> {
|
|
|
63
71
|
.and_then(provider_from_command_text)
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
fn provider_from_process_tree_argv(
|
|
75
|
+
root_pid: u32,
|
|
76
|
+
provider_from_argv_pid: &impl Fn(u32) -> Option<Provider>,
|
|
77
|
+
) -> Option<Provider> {
|
|
78
|
+
crate::platform::process::process_tree(root_pid)
|
|
79
|
+
.ok()?
|
|
80
|
+
.into_iter()
|
|
81
|
+
.filter(|pid| *pid != root_pid)
|
|
82
|
+
.find_map(provider_from_argv_pid)
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
67
86
|
process_environment(pid)
|
|
68
87
|
.as_deref()
|
|
@@ -200,7 +200,7 @@ pub fn register_binding_from_state_best_effort(
|
|
|
200
200
|
team: Option<&str>,
|
|
201
201
|
source: &str,
|
|
202
202
|
) -> Option<RegistryWriteOutcome> {
|
|
203
|
-
let Ok(state) = crate::state::persist::
|
|
203
|
+
let Ok(state) = crate::state::persist::load_runtime_state_without_migrations(workspace) else {
|
|
204
204
|
return None;
|
|
205
205
|
};
|
|
206
206
|
let team_key = match team.filter(|team| !team.is_empty()) {
|
|
@@ -376,9 +376,13 @@ fn spawn_agents(
|
|
|
376
376
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
377
377
|
// Python providers.py:145 + launch/core.py:253 — fresh launch runs the worker
|
|
378
378
|
// with cwd=workspace, same as the RS fork/add and restart paths.
|
|
379
|
-
let env_unset
|
|
380
|
-
profile_launch.env_unset.iter().cloned().collect(),
|
|
379
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
381
380
|
provider,
|
|
381
|
+
&mut env,
|
|
382
|
+
extend_worker_env_unset_for_effort(
|
|
383
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
384
|
+
provider,
|
|
385
|
+
),
|
|
382
386
|
);
|
|
383
387
|
// BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
|
|
384
388
|
// per-worker AGENTS.md(B2 灵魂件降级):写
|
|
@@ -3975,6 +3979,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
3975
3979
|
pre_spec_text.as_deref(),
|
|
3976
3980
|
pre_runtime_state.as_ref(),
|
|
3977
3981
|
agent_id,
|
|
3982
|
+
"state_upsert_failed",
|
|
3978
3983
|
);
|
|
3979
3984
|
return Err(error);
|
|
3980
3985
|
}
|
|
@@ -3996,6 +4001,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
3996
4001
|
pre_spec_text.as_deref(),
|
|
3997
4002
|
pre_runtime_state.as_ref(),
|
|
3998
4003
|
agent_id,
|
|
4004
|
+
"start_agent_failed",
|
|
3999
4005
|
);
|
|
4000
4006
|
return Err(error);
|
|
4001
4007
|
}
|
|
@@ -4012,6 +4018,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
4012
4018
|
pre_spec_text.as_deref(),
|
|
4013
4019
|
pre_runtime_state.as_ref(),
|
|
4014
4020
|
agent_id,
|
|
4021
|
+
"added_agent_paused",
|
|
4015
4022
|
);
|
|
4016
4023
|
return Err(LifecycleError::RequirementUnmet(format!(
|
|
4017
4024
|
"added agent {agent_id} is paused"
|
|
@@ -4037,12 +4044,21 @@ fn rollback_add_agent_atomic(
|
|
|
4037
4044
|
pre_spec_text: Option<&str>,
|
|
4038
4045
|
pre_runtime_state: Option<&serde_json::Value>,
|
|
4039
4046
|
agent_id: &AgentId,
|
|
4047
|
+
reason: &str,
|
|
4040
4048
|
) {
|
|
4041
|
-
if let Some(text) = pre_spec_text {
|
|
4042
|
-
|
|
4049
|
+
let spec_restored = if let Some(text) = pre_spec_text {
|
|
4050
|
+
std::fs::write(spec_path, text).is_ok()
|
|
4043
4051
|
} else {
|
|
4044
|
-
|
|
4045
|
-
|
|
4052
|
+
std::fs::remove_file(spec_path)
|
|
4053
|
+
.or_else(|error| {
|
|
4054
|
+
if error.kind() == std::io::ErrorKind::NotFound {
|
|
4055
|
+
Ok(())
|
|
4056
|
+
} else {
|
|
4057
|
+
Err(error)
|
|
4058
|
+
}
|
|
4059
|
+
})
|
|
4060
|
+
.is_ok()
|
|
4061
|
+
};
|
|
4046
4062
|
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.4):
|
|
4047
4063
|
// rollback must tombstone the newly-added agent so the persist merge
|
|
4048
4064
|
// does not re-attach a `roster_stub` from the latest on disk. Without
|
|
@@ -4050,12 +4066,13 @@ fn rollback_add_agent_atomic(
|
|
|
4050
4066
|
// survives the restore-from-pre_state pass and the retry sees
|
|
4051
4067
|
// "agent id already exists".
|
|
4052
4068
|
let deleted = [agent_id.as_str()];
|
|
4053
|
-
if let Some(state) = pre_runtime_state {
|
|
4054
|
-
|
|
4069
|
+
let state_restored = if let Some(state) = pre_runtime_state {
|
|
4070
|
+
crate::state::persist::save_runtime_state_with_deleted_agents(
|
|
4055
4071
|
run_workspace,
|
|
4056
4072
|
state,
|
|
4057
4073
|
&deleted,
|
|
4058
|
-
)
|
|
4074
|
+
)
|
|
4075
|
+
.is_ok()
|
|
4059
4076
|
} else {
|
|
4060
4077
|
// No prior runtime state — drop just the agent we added (load → strip → save).
|
|
4061
4078
|
if let Ok(mut state) = crate::state::persist::load_runtime_state(run_workspace) {
|
|
@@ -4078,13 +4095,27 @@ fn rollback_add_agent_atomic(
|
|
|
4078
4095
|
}
|
|
4079
4096
|
}
|
|
4080
4097
|
}
|
|
4081
|
-
|
|
4098
|
+
crate::state::persist::save_runtime_state_with_deleted_agents(
|
|
4082
4099
|
run_workspace,
|
|
4083
4100
|
&state,
|
|
4084
4101
|
&deleted,
|
|
4085
|
-
)
|
|
4102
|
+
)
|
|
4103
|
+
.is_ok()
|
|
4104
|
+
} else {
|
|
4105
|
+
false
|
|
4086
4106
|
}
|
|
4087
|
-
}
|
|
4107
|
+
};
|
|
4108
|
+
let rollback_ok = spec_restored && state_restored;
|
|
4109
|
+
let _ = crate::event_log::EventLog::new(run_workspace).write(
|
|
4110
|
+
"add_agent.rollback",
|
|
4111
|
+
serde_json::json!({
|
|
4112
|
+
"agent_id": agent_id.as_str(),
|
|
4113
|
+
"reason": reason,
|
|
4114
|
+
"rollback_ok": rollback_ok,
|
|
4115
|
+
"spec_restored": spec_restored,
|
|
4116
|
+
"state_restored": state_restored,
|
|
4117
|
+
}),
|
|
4118
|
+
);
|
|
4088
4119
|
}
|
|
4089
4120
|
|
|
4090
4121
|
fn upsert_agent_state_from_role(
|
|
@@ -4529,9 +4560,13 @@ pub fn fork_agent_with_transport(
|
|
|
4529
4560
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
4530
4561
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
4531
4562
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
4532
|
-
let env_unset
|
|
4533
|
-
profile_launch.env_unset.iter().cloned().collect(),
|
|
4563
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
4534
4564
|
provider,
|
|
4565
|
+
&mut env,
|
|
4566
|
+
extend_worker_env_unset_for_effort(
|
|
4567
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
4568
|
+
provider,
|
|
4569
|
+
),
|
|
4535
4570
|
);
|
|
4536
4571
|
let spawn_result = if session_live {
|
|
4537
4572
|
transport.spawn_into_with_env_unset(
|
|
@@ -183,9 +183,13 @@ pub(super) fn spawn_agent_window(
|
|
|
183
183
|
// 0.4.x provider effort MVP step 9: scrub CLAUDE_EFFORT for Claude
|
|
184
184
|
// worker spawn so a parent shell env cannot silently override the
|
|
185
185
|
// framework's effort decision.
|
|
186
|
-
let env_unset
|
|
187
|
-
profile_launch.env_unset.iter().cloned().collect(),
|
|
186
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
188
187
|
provider,
|
|
188
|
+
&mut env,
|
|
189
|
+
crate::lifecycle::launch::extend_worker_env_unset_for_effort(
|
|
190
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
191
|
+
provider,
|
|
192
|
+
),
|
|
189
193
|
);
|
|
190
194
|
|
|
191
195
|
// 0.4.6 Stage 2: write actual spawn plan event BEFORE invoking the
|
|
@@ -545,6 +545,9 @@ fn normalize_value(value: Value, ctx: &mut NormalizeCtx, key: Option<&str>) -> V
|
|
|
545
545
|
if matches!(key, Some("env_overlay_keys" | "env_unset_keys")) {
|
|
546
546
|
return json!("<ENV_KEYS>");
|
|
547
547
|
}
|
|
548
|
+
if matches!(key, Some("env_unset")) {
|
|
549
|
+
return json!([]);
|
|
550
|
+
}
|
|
548
551
|
match value {
|
|
549
552
|
Value::Object(map) => {
|
|
550
553
|
let sorted = map.into_iter().collect::<BTreeMap<_, _>>();
|
|
@@ -99,6 +99,7 @@ mod unix_impl {
|
|
|
99
99
|
//! `cli/mod.rs`, `coordinator/backoff.rs`, `mcp_server/wire.rs`,
|
|
100
100
|
//! `lifecycle/restart/agent.rs`) with zero behavioral drift.
|
|
101
101
|
use super::*;
|
|
102
|
+
use std::process::Command;
|
|
102
103
|
|
|
103
104
|
pub fn current_parent_pid() -> Option<u32> {
|
|
104
105
|
// Byte-equivalent to `mcp_server/wire.rs:319` and
|
|
@@ -150,8 +151,38 @@ mod unix_impl {
|
|
|
150
151
|
Ok(Vec::new())
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
pub fn process_tree(
|
|
154
|
-
|
|
154
|
+
pub fn process_tree(root: u32) -> Result<Vec<u32>, io::Error> {
|
|
155
|
+
let output = Command::new("ps").args(["-axo", "pid=,ppid="]).output()?;
|
|
156
|
+
if !output.status.success() {
|
|
157
|
+
return Err(io::Error::new(
|
|
158
|
+
io::ErrorKind::Other,
|
|
159
|
+
"ps_parent exited unsuccessfully",
|
|
160
|
+
));
|
|
161
|
+
}
|
|
162
|
+
let pairs = String::from_utf8_lossy(&output.stdout)
|
|
163
|
+
.lines()
|
|
164
|
+
.filter_map(|line| {
|
|
165
|
+
let mut parts = line.split_whitespace();
|
|
166
|
+
let pid = parts.next()?.parse::<u32>().ok()?;
|
|
167
|
+
let ppid = parts.next()?.parse::<u32>().ok()?;
|
|
168
|
+
Some((pid, ppid))
|
|
169
|
+
})
|
|
170
|
+
.collect::<Vec<_>>();
|
|
171
|
+
let mut out = Vec::new();
|
|
172
|
+
collect_child_pids(root, &pairs, &mut out);
|
|
173
|
+
out.push(root);
|
|
174
|
+
out.sort_unstable();
|
|
175
|
+
out.dedup();
|
|
176
|
+
Ok(out)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn collect_child_pids(parent: u32, pairs: &[(u32, u32)], out: &mut Vec<u32>) {
|
|
180
|
+
for (pid, ppid) in pairs {
|
|
181
|
+
if *ppid == parent && !out.contains(pid) {
|
|
182
|
+
out.push(*pid);
|
|
183
|
+
collect_child_pids(*pid, pairs, out);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
155
186
|
}
|
|
156
187
|
|
|
157
188
|
/// Send a SIGTERM (`TerminateGraceful`) or SIGKILL
|
|
@@ -1130,6 +1130,34 @@ pub fn load_runtime_state(workspace: &Path) -> Result<Value, StateError> {
|
|
|
1130
1130
|
Ok(state)
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
|
+
pub(crate) fn load_runtime_state_without_migrations(workspace: &Path) -> Result<Value, StateError> {
|
|
1134
|
+
let path = runtime_state_path(workspace);
|
|
1135
|
+
if !path.exists() {
|
|
1136
|
+
return Ok(
|
|
1137
|
+
json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null}),
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
let text = std::fs::read_to_string(&path)?;
|
|
1141
|
+
let mut state: Value = serde_json::from_str(&text)?;
|
|
1142
|
+
normalize_agent_session_state(&mut state);
|
|
1143
|
+
Ok(state)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
pub(crate) fn save_runtime_state_without_migrations(
|
|
1147
|
+
workspace: &Path,
|
|
1148
|
+
state: &Value,
|
|
1149
|
+
) -> Result<(), StateError> {
|
|
1150
|
+
let path = runtime_state_path(workspace);
|
|
1151
|
+
let _lock = RuntimeLock::acquire(workspace, "state-save-raw", 2.0)?;
|
|
1152
|
+
if let Some(parent) = path.parent() {
|
|
1153
|
+
std::fs::create_dir_all(parent)?;
|
|
1154
|
+
}
|
|
1155
|
+
let payload = serde_json::to_string_pretty(state)?;
|
|
1156
|
+
std::fs::write(&path, payload.as_bytes())?;
|
|
1157
|
+
cache_set(&path, state);
|
|
1158
|
+
Ok(())
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1133
1161
|
#[cfg(test)]
|
|
1134
1162
|
mod tests {
|
|
1135
1163
|
#![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
|
|
@@ -47,6 +47,7 @@ use super::persist::{
|
|
|
47
47
|
save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip as helper_write_root_with_lifecycle_topology_authority_and_capture_backfill_skip,
|
|
48
48
|
save_runtime_state_with_team_tombstone_lifecycle_topology_authority as helper_write_root_with_team_tombstone_lifecycle_topology_authority,
|
|
49
49
|
save_runtime_state_with_team_tombstoned_agents as helper_write_root_with_team_tombstoned_agents,
|
|
50
|
+
save_runtime_state_without_migrations as helper_write_root_without_migrations,
|
|
50
51
|
};
|
|
51
52
|
use super::projection::{
|
|
52
53
|
resolve_team_scoped_state as helper_resolve_team_scoped,
|
|
@@ -173,6 +174,9 @@ pub enum StateWriteIntent<'a> {
|
|
|
173
174
|
ClaimLeader {
|
|
174
175
|
team_key: &'a str,
|
|
175
176
|
},
|
|
177
|
+
LeaderBindingRestoreNonTargetTeams {
|
|
178
|
+
target_team_key: &'a str,
|
|
179
|
+
},
|
|
176
180
|
LeaderStartBinding {
|
|
177
181
|
team_key: &'a str,
|
|
178
182
|
transport_kind: &'a str,
|
|
@@ -325,6 +329,9 @@ fn route_direct(
|
|
|
325
329
|
// scoped preserve-claim-fields variant at :1702 uses the team-tombstoned
|
|
326
330
|
// agents helper.
|
|
327
331
|
StateWriteIntent::ClaimLeader { .. } => helper_write_root(workspace, state),
|
|
332
|
+
StateWriteIntent::LeaderBindingRestoreNonTargetTeams { .. } => {
|
|
333
|
+
helper_write_root_without_migrations(workspace, state)
|
|
334
|
+
}
|
|
328
335
|
// LeaderStartBinding -> managed/exec/external all root-save at
|
|
329
336
|
// leader/start.rs:795/903/946.
|
|
330
337
|
StateWriteIntent::LeaderStartBinding { .. } => helper_write_root(workspace, state),
|
|
@@ -457,6 +457,19 @@ fn session_exists_on_endpoint(endpoint: &str, session: &str) -> bool {
|
|
|
457
457
|
session_exists_on_endpoint_checked(endpoint, session).unwrap_or(false)
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
pub(crate) fn team_session_ready_on_endpoint(endpoint: &str, session: &str) -> Option<bool> {
|
|
461
|
+
if endpoint.is_empty() || session.is_empty() {
|
|
462
|
+
return None;
|
|
463
|
+
}
|
|
464
|
+
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint);
|
|
465
|
+
let targets = backend.list_targets().ok()?;
|
|
466
|
+
Some(
|
|
467
|
+
targets
|
|
468
|
+
.iter()
|
|
469
|
+
.any(|target| target.session.as_str() == session),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
460
473
|
fn session_exists_on_endpoint_checked(endpoint: &str, session: &str) -> Option<bool> {
|
|
461
474
|
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
|
|
462
475
|
.has_session(&SessionName::new(session.to_string()))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.44",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.44",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.44",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.44"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|