@team-agent/installer 0.3.16 → 0.3.17

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 CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.16"
569
+ version = "0.3.17"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.16"
12
+ version = "0.3.17"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -3019,7 +3019,7 @@ pub mod leader_port {
3019
3019
  provider: crate::provider::Provider,
3020
3020
  _confirm: bool,
3021
3021
  ) -> Result<Value, CliError> {
3022
- let result = crate::leader::attach_leader(workspace, pane, provider)
3022
+ let result = crate::leader::attach_leader(workspace, team, pane, provider)
3023
3023
  .map_err(|e| CliError::Runtime(e.to_string()))?;
3024
3024
  let requeued =
3025
3025
  attach_requeued_exhausted_watchers(workspace, result.bound_pane_id.as_ref())?;
@@ -90,6 +90,16 @@ use super::*;
90
90
  );
91
91
  }
92
92
 
93
+ #[test]
94
+ fn leader_launcher_args_external_leader_after_dashdash_is_provider_arg() {
95
+ let got = leader_launcher_args(&["--".into(), "--external-leader".into()]).unwrap();
96
+ assert!(!got.external_leader);
97
+ assert_eq!(
98
+ got.provider_args,
99
+ vec!["--".to_string(), "--external-leader".to_string()]
100
+ );
101
+ }
102
+
93
103
  #[test]
94
104
  fn leader_launcher_args_attach_session_spaced() {
95
105
  // golden: ["--attach-session","mysess"] -> attach_session="mysess"
@@ -1,6 +1,7 @@
1
1
  //! leader::lease — attach / claim / autobind 统一 CAS 路径 + claim_lease_no_incident
2
2
  //! + 双写 / 分叉检测。
3
3
 
4
+ use std::collections::BTreeSet;
4
5
  use std::path::Path;
5
6
 
6
7
  use serde_json::{json, Value};
@@ -25,27 +26,34 @@ use super::{
25
26
  /// 整段临界区做 state 变更 + 事件 + 双写 + requeue exhausted watchers。
26
27
  pub fn attach_leader(
27
28
  workspace: &Path,
29
+ team: Option<&str>,
28
30
  pane: Option<&PaneId>,
29
31
  provider: Provider,
30
32
  ) -> Result<LeaseResult, LeaderError> {
31
33
  let event_log = crate::event_log::EventLog::new(workspace);
32
- let mut state = crate::state::persist::load_runtime_state(workspace)?;
33
- let targets = crate::tmux_backend::TmuxBackend::for_workspace(workspace)
34
- .list_targets()
35
- .unwrap_or_default();
34
+ let scoped_team = team.filter(|value| !value.is_empty());
35
+ let mut state = if scoped_team.is_some() {
36
+ crate::state::projection::select_runtime_state(workspace, scoped_team)?
37
+ } else {
38
+ crate::state::persist::load_runtime_state(workspace)?
39
+ };
40
+ let targets = attach_leader_targets(workspace, &state);
36
41
  let pane_id = pane
37
42
  .cloned()
38
43
  .or_else(|| std::env::var("TMUX_PANE").ok().filter(|p| !p.is_empty()).map(PaneId::new))
39
44
  .ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
40
45
  let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
41
- let Some(target) = targets.iter().find(|target| target.pane_id == pane_id) else {
46
+ let Some(target) = targets.iter().find(|target| target.info.pane_id == pane_id) else {
42
47
  return Err(LeaderError::Validation(format!("tmux pane not found: {pane_id}")));
43
48
  };
44
- let mut receiver = receiver_for_attach_target(workspace, &state, target, provider, Discovery::ExplicitPane)?;
45
- let validation = validate_attach_target(workspace, &state, target);
49
+ let mut receiver = receiver_for_attach_target(workspace, &state, &target.info, provider, Discovery::ExplicitPane)?;
50
+ if let Some(endpoint) = target.endpoint.as_ref() {
51
+ receiver.tmux_socket = Some(endpoint.clone());
52
+ }
53
+ let validation = validate_attach_target(workspace, &state, &target.info);
46
54
  if validation.is_err() {
47
- let pane_info = pane_info_value(target);
48
- let targets_value = Value::Array(targets.iter().map(pane_info_value).collect());
55
+ let pane_info = pane_info_value(&target.info);
56
+ let targets_value = Value::Array(targets.iter().map(|target| pane_info_value(&target.info)).collect());
49
57
  let owner_record = state_owner(&state);
50
58
  if let Some((readopted, validation)) = crate::leader::try_readopt_leader_pane(
51
59
  workspace,
@@ -85,7 +93,7 @@ pub fn attach_leader(
85
93
  let epoch = current_owner_epoch(&state);
86
94
  if state.get("team_owner").is_some() {
87
95
  write_receiver_to_state(&mut state, &receiver)?;
88
- write_lease_dual_state(workspace, &state)?;
96
+ write_claim_state(workspace, &state, scoped_team, None)?;
89
97
  event_log.write(
90
98
  super::LeaderEvent::ReceiverAttached.name(),
91
99
  json!({"pane_id": pane_id.as_str(), "owner_epoch": epoch.0}),
@@ -108,7 +116,7 @@ pub fn attach_leader(
108
116
  receiver.leader_session_uuid = Some(identity.leader_session_uuid.clone());
109
117
  let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
110
118
  write_binding_to_state(&mut state, &receiver, &owner)?;
111
- write_lease_dual_state(workspace, &state)?;
119
+ write_claim_state(workspace, &state, scoped_team, None)?;
112
120
  event_log.write(
113
121
  super::LeaderEvent::ReceiverAttached.name(),
114
122
  json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
@@ -126,6 +134,80 @@ pub fn attach_leader(
126
134
  })
127
135
  }
128
136
 
137
+ #[derive(Clone)]
138
+ struct AttachLeaderTarget {
139
+ info: PaneInfo,
140
+ endpoint: Option<String>,
141
+ }
142
+
143
+ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTarget> {
144
+ let mut targets = Vec::new();
145
+ for endpoint in state_recorded_tmux_endpoints(state) {
146
+ let backend = tmux_backend_for_endpoint(&endpoint);
147
+ targets.extend(
148
+ backend
149
+ .list_targets()
150
+ .unwrap_or_default()
151
+ .into_iter()
152
+ .map(|info| AttachLeaderTarget {
153
+ info,
154
+ endpoint: Some(endpoint.clone()),
155
+ }),
156
+ );
157
+ }
158
+ targets.extend(
159
+ crate::tmux_backend::TmuxBackend::for_workspace(workspace)
160
+ .list_targets()
161
+ .unwrap_or_default()
162
+ .into_iter()
163
+ .map(|info| AttachLeaderTarget { info, endpoint: None }),
164
+ );
165
+ targets
166
+ }
167
+
168
+ fn tmux_backend_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
169
+ if endpoint.is_empty() || endpoint == "default" {
170
+ crate::tmux_backend::TmuxBackend::new()
171
+ } else if Path::new(endpoint).is_absolute() {
172
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
173
+ } else {
174
+ crate::tmux_backend::TmuxBackend::for_socket_name(endpoint)
175
+ }
176
+ }
177
+
178
+ fn state_recorded_tmux_endpoints(state: &Value) -> BTreeSet<String> {
179
+ let mut out = BTreeSet::new();
180
+ push_state_tmux_endpoints(state, &mut out);
181
+ if let Some(teams) = state.get("teams").and_then(Value::as_object) {
182
+ for team_state in teams.values() {
183
+ push_state_tmux_endpoints(team_state, &mut out);
184
+ }
185
+ }
186
+ out
187
+ }
188
+
189
+ fn push_state_tmux_endpoints(state: &Value, out: &mut BTreeSet<String>) {
190
+ for key in ["tmux_socket", "tmux_endpoint"] {
191
+ if let Some(endpoint) = state
192
+ .get(key)
193
+ .and_then(Value::as_str)
194
+ .filter(|value| !value.is_empty())
195
+ {
196
+ out.insert(endpoint.to_string());
197
+ }
198
+ }
199
+ for key in ["team_owner", "leader_receiver"] {
200
+ if let Some(endpoint) = state
201
+ .get(key)
202
+ .and_then(|value| value.get("tmux_socket"))
203
+ .and_then(Value::as_str)
204
+ .filter(|value| !value.is_empty())
205
+ {
206
+ out.insert(endpoint.to_string());
207
+ }
208
+ }
209
+ }
210
+
129
211
  fn requeue_exhausted_watchers_after_attach(
130
212
  workspace: &Path,
131
213
  state: &Value,
@@ -205,6 +205,9 @@ pub fn execute_leader_plan(
205
205
  .unwrap_or_else(|| "signal".to_string())
206
206
  )));
207
207
  }
208
+ if plan.is_external_leader {
209
+ persist_external_leader_topology_marker(plan, workspace)?;
210
+ }
208
211
  if detached {
209
212
  Ok(LeaderLaunchOutcome {
210
213
  status: LeaderLaunchStatus::Detached,
@@ -529,6 +532,41 @@ fn persist_managed_leader_binding(
529
532
  Ok(())
530
533
  }
531
534
 
535
+ fn persist_external_leader_topology_marker(
536
+ plan: &LeaderStartPlan,
537
+ workspace: &Path,
538
+ ) -> Result<(), LeaderError> {
539
+ let identity = plan
540
+ .identity
541
+ .as_ref()
542
+ .ok_or_else(|| LeaderError::Start("external leader identity missing".to_string()))?;
543
+ let mut state = crate::state::persist::load_runtime_state(workspace)
544
+ .unwrap_or_else(|_| serde_json::json!({}));
545
+ if let Some(obj) = state.as_object_mut() {
546
+ obj.entry("workspace".to_string()).or_insert_with(|| {
547
+ serde_json::json!(resolve_workspace_for_hash(workspace).to_string_lossy().to_string())
548
+ });
549
+ obj.entry("active_team_key".to_string())
550
+ .or_insert_with(|| serde_json::json!(identity.team_id.as_str()));
551
+ if let Some(session) = plan.session_name.as_ref() {
552
+ obj.entry("session_name".to_string())
553
+ .or_insert_with(|| serde_json::json!(session.as_str()));
554
+ }
555
+ obj.insert("is_external_leader".to_string(), serde_json::json!(true));
556
+ }
557
+ let entry = crate::state::projection::compact_team_state(&state);
558
+ if let Some(obj) = state.as_object_mut() {
559
+ let teams = obj
560
+ .entry("teams".to_string())
561
+ .or_insert_with(|| serde_json::json!({}));
562
+ if let Some(teams) = teams.as_object_mut() {
563
+ teams.insert(identity.team_id.as_str().to_string(), entry);
564
+ }
565
+ }
566
+ crate::state::persist::save_runtime_state(workspace, &state)?;
567
+ Ok(())
568
+ }
569
+
532
570
  fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
533
571
  if argv.iter().any(|arg| arg == "-d") {
534
572
  return false;
@@ -14,7 +14,7 @@ use super::*;
14
14
  fn attach_leader_errors_when_pane_not_resolvable() {
15
15
  let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
16
16
  std::fs::create_dir_all(&ws).unwrap();
17
- let r = attach_leader(&ws, Some(&PaneId::new("%1")), Provider::Codex);
17
+ let r = attach_leader(&ws, None, Some(&PaneId::new("%1")), Provider::Codex);
18
18
  // 不可解析 pane → Err(Validation),错误串提及 pane not found。
19
19
  match r {
20
20
  Err(LeaderError::Validation(msg)) => {
@@ -39,7 +39,7 @@ use super::*;
39
39
  let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
40
40
  std::fs::create_dir_all(&ws).unwrap();
41
41
  let pane = PaneId::new("%1");
42
- let r = attach_leader(&ws, Some(&pane), Provider::Codex).unwrap();
42
+ let r = attach_leader(&ws, None, Some(&pane), Provider::Codex).unwrap();
43
43
  assert!(r.ok);
44
44
  assert_eq!(r.status, LeaseStatus::Claimed);
45
45
  let owner = r.owner.as_ref().expect("attach 成功必带 owner");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
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.3.16",
24
- "@team-agent/cli-darwin-x64": "0.3.16",
25
- "@team-agent/cli-linux-x64": "0.3.16"
23
+ "@team-agent/cli-darwin-arm64": "0.3.17",
24
+ "@team-agent/cli-darwin-x64": "0.3.17",
25
+ "@team-agent/cli-linux-x64": "0.3.17"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",