@team-agent/installer 0.5.43 → 0.5.45
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/emit.rs +44 -33
- package/crates/team-agent/src/cli/mod.rs +99 -9
- package/crates/team-agent/src/cli/named_address.rs +206 -4
- package/crates/team-agent/src/cli/send.rs +105 -2
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/run_delegation.rs +7 -4
- 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/mcp_server/tools.rs +67 -8
- package/crates/team-agent/src/model/mod.rs +6 -0
- package/crates/team-agent/src/model/name_similarity.rs +266 -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
|
@@ -55,8 +55,17 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
55
55
|
"--to-name requires a non-empty message".to_string(),
|
|
56
56
|
));
|
|
57
57
|
}
|
|
58
|
+
// 0.5.45 naming-addressing (design §3.1, RED-1): thread
|
|
59
|
+
// `--team` down to resolver so bare `--to-name agent --team T`
|
|
60
|
+
// scopes to `T` BEFORE workspace scanning. Qualified addresses
|
|
61
|
+
// (`team/agent`, `workspace::team/agent`) ignore the scope
|
|
62
|
+
// per §1 priority ladder.
|
|
58
63
|
let (resolved, transport) =
|
|
59
|
-
match crate::cli::named_address::resolve_name_for_cli(
|
|
64
|
+
match crate::cli::named_address::resolve_name_for_cli(
|
|
65
|
+
&args.workspace,
|
|
66
|
+
to_name,
|
|
67
|
+
args.team.as_deref(),
|
|
68
|
+
) {
|
|
60
69
|
Ok(resolved) => resolved,
|
|
61
70
|
Err(error) => {
|
|
62
71
|
// E6 (0.5.9 offline-mailbox-toname-design §3.1/§6.2): when
|
|
@@ -166,6 +175,15 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
166
175
|
outcome = observe_initial_delivery_for_watch(&selected, &target, &outcome, &opts)?;
|
|
167
176
|
}
|
|
168
177
|
let mut value = delivery_outcome_json(&outcome, &target, &content, &opts);
|
|
178
|
+
// 0.5.45 naming-addressing (design §3.5, RED-2/RED-3 positional):
|
|
179
|
+
// when the refusal reason is `target_not_in_team` AND the target
|
|
180
|
+
// was a Single non-special short id, attach scope-safe
|
|
181
|
+
// suggestions ranked from `selected.state.agents` — never from
|
|
182
|
+
// raw workspace `teams` (design §3.5 & risk table). Zero DB
|
|
183
|
+
// write / zero inject: the request stays refused, only the JSON
|
|
184
|
+
// envelope gains `requested_name`/`suggested_name`/`candidates`
|
|
185
|
+
// and the human `Action` gains a "Did you mean" line.
|
|
186
|
+
attach_positional_typo_suggestions(&mut value, &target, &selected.state);
|
|
169
187
|
append_loud_ensure_fields(&mut value, coordinator_ensure.as_ref());
|
|
170
188
|
if opts.watch_result && initial_delivery_allows_watch(outcome.status) {
|
|
171
189
|
if let Some(obj) = value.as_object_mut() {
|
|
@@ -773,6 +791,67 @@ fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> Value {
|
|
|
773
791
|
})
|
|
774
792
|
}
|
|
775
793
|
|
|
794
|
+
/// 0.5.45 naming-addressing (design §3.5, RED-2/RED-3 positional):
|
|
795
|
+
/// after `messaging::send_message` refuses with `target_not_in_team`
|
|
796
|
+
/// for a Single non-special short id, attach scope-safe advisory
|
|
797
|
+
/// suggestions to the outbound JSON envelope. Candidate source =
|
|
798
|
+
/// selected team's projected `agents` map (never the raw workspace
|
|
799
|
+
/// `teams`) so sibling teams cannot leak. Zero DB write, zero inject
|
|
800
|
+
/// — the refusal exit code is unchanged.
|
|
801
|
+
fn attach_positional_typo_suggestions(
|
|
802
|
+
value: &mut Value,
|
|
803
|
+
target: &MessageTarget,
|
|
804
|
+
selected_state: &Value,
|
|
805
|
+
) {
|
|
806
|
+
use crate::model::name_similarity::{rank, Candidate};
|
|
807
|
+
let requested = match target {
|
|
808
|
+
MessageTarget::Single(id) if id != "*" && id != "leader" => id.clone(),
|
|
809
|
+
_ => return,
|
|
810
|
+
};
|
|
811
|
+
let Some(obj) = value.as_object_mut() else {
|
|
812
|
+
return;
|
|
813
|
+
};
|
|
814
|
+
if obj.get("reason").and_then(Value::as_str) != Some("target_not_in_team") {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
let team_key = selected_state
|
|
818
|
+
.get("active_team_key")
|
|
819
|
+
.or_else(|| selected_state.get("team_key"))
|
|
820
|
+
.and_then(Value::as_str)
|
|
821
|
+
.unwrap_or("");
|
|
822
|
+
let candidates: Vec<Candidate<String>> = selected_state
|
|
823
|
+
.get("agents")
|
|
824
|
+
.and_then(Value::as_object)
|
|
825
|
+
.map(|agents| {
|
|
826
|
+
agents
|
|
827
|
+
.keys()
|
|
828
|
+
.map(|agent_id| Candidate {
|
|
829
|
+
match_key: agent_id.clone(),
|
|
830
|
+
stable_key: agent_id.clone(),
|
|
831
|
+
payload: agent_id.clone(),
|
|
832
|
+
})
|
|
833
|
+
.collect()
|
|
834
|
+
})
|
|
835
|
+
.unwrap_or_default();
|
|
836
|
+
let ranked = rank(&requested, &candidates);
|
|
837
|
+
let candidate_values: Vec<Value> = ranked
|
|
838
|
+
.iter()
|
|
839
|
+
.map(|agent_id| {
|
|
840
|
+
json!({
|
|
841
|
+
"name": agent_id,
|
|
842
|
+
"team_key": team_key,
|
|
843
|
+
"agent_id": agent_id,
|
|
844
|
+
"advisory": true,
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
.collect();
|
|
848
|
+
obj.insert("requested_name".to_string(), json!(requested));
|
|
849
|
+
if let Some(best) = ranked.first() {
|
|
850
|
+
obj.insert("suggested_name".to_string(), json!(best));
|
|
851
|
+
}
|
|
852
|
+
obj.insert("candidates".to_string(), Value::Array(candidate_values));
|
|
853
|
+
}
|
|
854
|
+
|
|
776
855
|
fn delivery_outcome_json(
|
|
777
856
|
outcome: &DeliveryOutcome,
|
|
778
857
|
target: &MessageTarget,
|
|
@@ -973,6 +1052,25 @@ fn send_human_output(value: &Value) -> String {
|
|
|
973
1052
|
parts.push(send_human_field(value, key));
|
|
974
1053
|
}
|
|
975
1054
|
}
|
|
1055
|
+
// 0.5.45 naming-addressing (design §3.4/§3.5, RED-3 positional):
|
|
1056
|
+
// when the refusal envelope carries a scope-safe suggestion,
|
|
1057
|
+
// surface it verbatim in human output so users can copy the
|
|
1058
|
+
// right short id. `requested_name` echoes the typo, `suggested_
|
|
1059
|
+
// name` is the copyable canonical.
|
|
1060
|
+
if let Some(requested) = value
|
|
1061
|
+
.get("requested_name")
|
|
1062
|
+
.and_then(Value::as_str)
|
|
1063
|
+
.filter(|s| !s.is_empty())
|
|
1064
|
+
{
|
|
1065
|
+
parts.push(format!("requested_name: {requested}"));
|
|
1066
|
+
}
|
|
1067
|
+
if let Some(suggested) = value
|
|
1068
|
+
.get("suggested_name")
|
|
1069
|
+
.and_then(Value::as_str)
|
|
1070
|
+
.filter(|s| !s.is_empty())
|
|
1071
|
+
{
|
|
1072
|
+
parts.push(format!("Did you mean `{suggested}`? suggested_name: {suggested}"));
|
|
1073
|
+
}
|
|
976
1074
|
parts.join(" ")
|
|
977
1075
|
}
|
|
978
1076
|
|
|
@@ -1250,8 +1348,13 @@ pub fn send_to_canonical_leader_target(
|
|
|
1250
1348
|
// synthesized `<workspace>::<team_key>/leader` name so live inject +
|
|
1251
1349
|
// mailbox both go through one code path.
|
|
1252
1350
|
let to_name = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
1351
|
+
// 0.5.45 naming-addressing (design §3.1 / §4.1): internal
|
|
1352
|
+
// registry-to-E6 delegation MUST pass None for bare_team_scope.
|
|
1353
|
+
// The synthesized name above is a full `workspace::team/leader`
|
|
1354
|
+
// form, and the caller's `--team` flag (if any) must not
|
|
1355
|
+
// override the registry's authoritative team_key.
|
|
1253
1356
|
let (resolved, transport) =
|
|
1254
|
-
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name) {
|
|
1357
|
+
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name, None) {
|
|
1255
1358
|
Ok(r) => r,
|
|
1256
1359
|
Err(err) => {
|
|
1257
1360
|
// Named-address refusal — surface it verbatim but tag as
|
|
@@ -153,7 +153,7 @@ pub(crate) struct CommandSpec {
|
|
|
153
153
|
#[rustfmt::skip]
|
|
154
154
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
155
155
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
156
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "send a message/task", usage: "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
156
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "send a message/task", usage: "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME | --to-name agent | --to-name team/agent | --to-name workspace::team/agent] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
157
157
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
158
158
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
159
159
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -125,12 +125,15 @@ fn run_dispatches_send_to_handler_returns_ok() {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
#[test]
|
|
128
|
-
fn
|
|
129
|
-
//
|
|
128
|
+
fn run_unknown_subcommand_is_error_not_usage() {
|
|
129
|
+
// 0.5.45 naming-addressing (RED-6): unknown subcommand exits 1
|
|
130
|
+
// (Error), aligned with the shared refusal shape used by
|
|
131
|
+
// `send`/`--to-name` typos. Pre-0.5.45 argparse-style Usage (2)
|
|
132
|
+
// was internal drift.
|
|
130
133
|
assert_eq!(
|
|
131
134
|
run(&["totally-not-a-subcommand".to_string()], Path::new(".")),
|
|
132
|
-
ExitCode::
|
|
133
|
-
"
|
|
135
|
+
ExitCode::Error,
|
|
136
|
+
"unknown subcommand must map to ExitCode::Error (aligned with send/--to-name typo family)"
|
|
134
137
|
);
|
|
135
138
|
}
|
|
136
139
|
|
|
@@ -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<_, _>>();
|