@team-agent/installer 0.5.45 → 0.5.47

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.
@@ -14,6 +14,142 @@ pub(super) struct SpawnedAgentWindow {
14
14
  pub owner_team_id: Option<String>,
15
15
  }
16
16
 
17
+ #[derive(Clone)]
18
+ pub(super) struct SameRoleCohortTarget {
19
+ pub agent_id: String,
20
+ pub window: String,
21
+ pub expected_pane_id: Option<String>,
22
+ }
23
+
24
+ impl SameRoleCohortTarget {
25
+ pub(super) fn new(agent_id: &AgentId, window: &str) -> Self {
26
+ Self {
27
+ agent_id: agent_id.as_str().to_string(),
28
+ window: window.to_string(),
29
+ expected_pane_id: None,
30
+ }
31
+ }
32
+
33
+ pub(super) fn with_expected_pane_id(mut self, pane_id: Option<&str>) -> Self {
34
+ self.expected_pane_id = pane_id.map(ToString::to_string);
35
+ self
36
+ }
37
+ }
38
+
39
+ pub(super) fn is_per_agent_cohort_window(window: &str, agent_id: &AgentId) -> bool {
40
+ window == agent_id.as_str() && !crate::lifecycle::launch::is_adaptive_layout_window_pub(window)
41
+ }
42
+
43
+ pub(super) fn same_role_cohort_pre_spawn_error(
44
+ transport: &dyn crate::transport::Transport,
45
+ session_name: &SessionName,
46
+ operation: &str,
47
+ targets: &[SameRoleCohortTarget],
48
+ ) -> Option<String> {
49
+ same_role_cohort_error(transport, session_name, operation, targets, 0, true)
50
+ }
51
+
52
+ pub(super) fn same_role_cohort_exactly_one_error(
53
+ transport: &dyn crate::transport::Transport,
54
+ session_name: &SessionName,
55
+ operation: &str,
56
+ targets: &[SameRoleCohortTarget],
57
+ ) -> Option<String> {
58
+ same_role_cohort_error(transport, session_name, operation, targets, 1, false)
59
+ }
60
+
61
+ pub(super) fn retire_expected_same_role_cohorts(
62
+ transport: &dyn crate::transport::Transport,
63
+ operation: &str,
64
+ targets: &[SameRoleCohortTarget],
65
+ ) -> Result<(), String> {
66
+ for target in targets {
67
+ let Some(pane_id) = target.expected_pane_id.as_deref() else {
68
+ continue;
69
+ };
70
+ let pane = crate::transport::PaneId::new(pane_id);
71
+ if transport.has_pane(&pane).ok().flatten() == Some(false) {
72
+ continue;
73
+ }
74
+ transport.kill_pane(&pane).map_err(|error| {
75
+ format!(
76
+ "{operation} failed to retire old same-role cohort {}:window={}:pane={pane_id}: {error}",
77
+ target.agent_id, target.window
78
+ )
79
+ })?;
80
+ }
81
+ Ok(())
82
+ }
83
+
84
+ fn same_role_cohort_error(
85
+ transport: &dyn crate::transport::Transport,
86
+ session_name: &SessionName,
87
+ operation: &str,
88
+ targets: &[SameRoleCohortTarget],
89
+ expected_live: usize,
90
+ expected_old_only: bool,
91
+ ) -> Option<String> {
92
+ let panes = transport.list_targets().ok()?;
93
+ let mut cardinality_proofs = Vec::new();
94
+ let mut binding_proofs = Vec::new();
95
+ for target in targets {
96
+ let live_panes = panes
97
+ .iter()
98
+ .filter(|pane| pane.session.as_str() == session_name.as_str())
99
+ .filter(|pane| {
100
+ pane.window_name
101
+ .as_ref()
102
+ .is_some_and(|window| window.as_str() == target.window)
103
+ })
104
+ .filter(|pane| {
105
+ !expected_old_only
106
+ || target
107
+ .expected_pane_id
108
+ .as_deref()
109
+ .is_some_and(|expected| pane.pane_id.as_str() == expected)
110
+ })
111
+ .filter(|pane| {
112
+ transport
113
+ .liveness(&pane.pane_id)
114
+ .is_ok_and(|live| live == crate::transport::PaneLiveness::Live)
115
+ })
116
+ .map(|pane| pane.pane_id.as_str().to_string())
117
+ .collect::<Vec<_>>();
118
+ if live_panes.len() != expected_live {
119
+ cardinality_proofs.push(format!(
120
+ "{}:window={}:live_panes=[{}]",
121
+ target.agent_id,
122
+ target.window,
123
+ live_panes.join(",")
124
+ ));
125
+ } else if !expected_old_only {
126
+ let observed = live_panes.first().map(String::as_str);
127
+ if target.expected_pane_id.as_deref() != observed {
128
+ binding_proofs.push(format!(
129
+ "{}:window={}:expected_pane={}:observed_pane={}",
130
+ target.agent_id,
131
+ target.window,
132
+ target.expected_pane_id.as_deref().unwrap_or("<missing>"),
133
+ observed.unwrap_or("<missing>")
134
+ ));
135
+ }
136
+ }
137
+ }
138
+ if !binding_proofs.is_empty() {
139
+ return Some(format!(
140
+ "{operation} refused: spawn identity/binding mismatch; {}",
141
+ binding_proofs.join("; ")
142
+ ));
143
+ }
144
+ if !cardinality_proofs.is_empty() {
145
+ return Some(format!(
146
+ "{operation} refused: same-role cohort duplicate proof failed; {}",
147
+ cardinality_proofs.join("; ")
148
+ ));
149
+ }
150
+ None
151
+ }
152
+
17
153
  #[allow(clippy::too_many_arguments)]
18
154
  pub(super) fn spawn_agent_window(
19
155
  workspace: &Path,
@@ -428,6 +428,18 @@ fn restart_with_selected_team_and_transport(
428
428
  let live_worker_session_deferred_teardown =
429
429
  session_live_or_default(transport, &session_name, false)
430
430
  && !collect_live_agents_from_state(&state).is_empty();
431
+ let same_role_cohort_targets =
432
+ same_role_cohort_targets_from_decisions(&state, spec_workspace, &plan.decisions);
433
+ let pre_spawn_pane_ids = if live_worker_session_deferred_teardown {
434
+ transport
435
+ .list_targets()
436
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?
437
+ .into_iter()
438
+ .map(|pane| pane.pane_id.as_str().to_string())
439
+ .collect::<std::collections::BTreeSet<_>>()
440
+ } else {
441
+ std::collections::BTreeSet::new()
442
+ };
431
443
  if session_live_or_default(transport, &session_name, false)
432
444
  && !live_worker_session_deferred_teardown
433
445
  {
@@ -779,6 +791,46 @@ fn restart_with_selected_team_and_transport(
779
791
  );
780
792
  }
781
793
  // END_B5_RESTART_ISOLATION_LOOP
794
+ if live_worker_session_deferred_teardown {
795
+ let successful_agent_ids = successful_agents
796
+ .iter()
797
+ .map(|agent| agent.agent_id.as_str())
798
+ .collect::<std::collections::BTreeSet<_>>();
799
+ let old_successful_targets = same_role_cohort_targets
800
+ .iter()
801
+ .filter(|target| successful_agent_ids.contains(target.agent_id.as_str()))
802
+ .cloned()
803
+ .collect::<Vec<_>>();
804
+ let successful_targets =
805
+ same_role_cohort_targets_from_decisions(&state, spec_workspace, &successful_agents);
806
+ let actual_spawn_panes = actual_successful_restart_spawn_panes(
807
+ transport,
808
+ &session_name,
809
+ &successful_targets,
810
+ &pre_spawn_pane_ids,
811
+ )?;
812
+ if let Some(error) =
813
+ actual_restart_spawn_binding_error(&successful_targets, &actual_spawn_panes)
814
+ {
815
+ cleanup_actual_restart_spawns(transport, &actual_spawn_panes);
816
+ return Err(LifecycleError::RequirementUnmet(error));
817
+ }
818
+ if let Err(error) =
819
+ retire_expected_same_role_cohorts(transport, "restart", &old_successful_targets)
820
+ {
821
+ cleanup_actual_restart_spawns(transport, &actual_spawn_panes);
822
+ return Err(LifecycleError::RequirementUnmet(error));
823
+ }
824
+ if let Some(error) = same_role_cohort_exactly_one_error(
825
+ transport,
826
+ &session_name,
827
+ "restart",
828
+ &successful_targets,
829
+ ) {
830
+ cleanup_actual_restart_spawns(transport, &actual_spawn_panes);
831
+ return Err(LifecycleError::RequirementUnmet(error));
832
+ }
833
+ }
782
834
  let mut topology_authority_agent_ids = successful_agents
783
835
  .iter()
784
836
  .map(|agent| agent.agent_id.as_str().to_string())
@@ -2090,6 +2142,122 @@ fn mark_fake_harness_agent_respawned(
2090
2142
  agent.insert("owner_team_id".to_string(), serde_json::json!(team_key));
2091
2143
  }
2092
2144
 
2145
+ fn same_role_cohort_targets_from_decisions(
2146
+ state: &serde_json::Value,
2147
+ spec_workspace: &Path,
2148
+ agents: &[RestartedAgent],
2149
+ ) -> Vec<SameRoleCohortTarget> {
2150
+ let mut targets = Vec::new();
2151
+ for restarted in agents {
2152
+ let agent_id = &restarted.agent_id;
2153
+ let raw_agent = state
2154
+ .get("agents")
2155
+ .and_then(|agents| agents.get(agent_id.as_str()));
2156
+ let effective_agent = raw_agent.map(|agent| {
2157
+ rehydrate_agent_command_context_from_spec(spec_workspace, agent_id, agent)
2158
+ });
2159
+ if effective_agent
2160
+ .as_ref()
2161
+ .and_then(|agent| agent.get("provider"))
2162
+ .and_then(serde_json::Value::as_str)
2163
+ .is_some_and(|provider| provider.eq_ignore_ascii_case("fake"))
2164
+ {
2165
+ continue;
2166
+ }
2167
+ let window = raw_agent
2168
+ .and_then(|agent| agent.get("window"))
2169
+ .and_then(serde_json::Value::as_str)
2170
+ .filter(|value| !value.is_empty())
2171
+ .unwrap_or_else(|| agent_id.as_str());
2172
+ if is_per_agent_cohort_window(window, agent_id) {
2173
+ let expected_pane_id = raw_agent
2174
+ .and_then(|agent| agent.get("pane_id"))
2175
+ .and_then(serde_json::Value::as_str)
2176
+ .filter(|value| !value.is_empty());
2177
+ targets.push(
2178
+ SameRoleCohortTarget::new(agent_id, window).with_expected_pane_id(expected_pane_id),
2179
+ );
2180
+ }
2181
+ }
2182
+ targets
2183
+ }
2184
+
2185
+ fn actual_successful_restart_spawn_panes(
2186
+ transport: &dyn crate::transport::Transport,
2187
+ session_name: &SessionName,
2188
+ targets: &[SameRoleCohortTarget],
2189
+ pre_spawn_pane_ids: &std::collections::BTreeSet<String>,
2190
+ ) -> Result<Vec<crate::transport::PaneInfo>, LifecycleError> {
2191
+ let windows = targets
2192
+ .iter()
2193
+ .map(|target| target.window.as_str())
2194
+ .collect::<std::collections::BTreeSet<_>>();
2195
+ let panes = transport
2196
+ .list_targets()
2197
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?
2198
+ .into_iter()
2199
+ .filter(|pane| !pre_spawn_pane_ids.contains(pane.pane_id.as_str()))
2200
+ .filter(|pane| pane.session.as_str() == session_name.as_str())
2201
+ .filter(|pane| {
2202
+ pane.window_name
2203
+ .as_ref()
2204
+ .is_some_and(|window| windows.contains(window.as_str()))
2205
+ })
2206
+ .filter(|pane| {
2207
+ transport
2208
+ .liveness(&pane.pane_id)
2209
+ .is_ok_and(|state| state == crate::transport::PaneLiveness::Live)
2210
+ })
2211
+ .collect();
2212
+ Ok(panes)
2213
+ }
2214
+
2215
+ fn actual_restart_spawn_binding_error(
2216
+ targets: &[SameRoleCohortTarget],
2217
+ actual_spawn_panes: &[crate::transport::PaneInfo],
2218
+ ) -> Option<String> {
2219
+ let mut mismatches = Vec::new();
2220
+ for target in targets {
2221
+ let observed = actual_spawn_panes
2222
+ .iter()
2223
+ .filter(|pane| {
2224
+ pane.window_name
2225
+ .as_ref()
2226
+ .is_some_and(|window| window.as_str() == target.window)
2227
+ })
2228
+ .map(|pane| pane.pane_id.as_str().to_string())
2229
+ .collect::<Vec<_>>();
2230
+ if observed.len() != 1
2231
+ || target.expected_pane_id.as_deref() != observed.first().map(String::as_str)
2232
+ {
2233
+ mismatches.push(format!(
2234
+ "{}:window={}:expected_pane={}:actual_spawn_panes=[{}]",
2235
+ target.agent_id,
2236
+ target.window,
2237
+ target.expected_pane_id.as_deref().unwrap_or("<missing>"),
2238
+ observed.join(",")
2239
+ ));
2240
+ }
2241
+ }
2242
+ if mismatches.is_empty() {
2243
+ None
2244
+ } else {
2245
+ Some(format!(
2246
+ "restart refused: spawn identity/binding mismatch; {}",
2247
+ mismatches.join("; ")
2248
+ ))
2249
+ }
2250
+ }
2251
+
2252
+ fn cleanup_actual_restart_spawns(
2253
+ transport: &dyn crate::transport::Transport,
2254
+ actual_spawn_panes: &[crate::transport::PaneInfo],
2255
+ ) {
2256
+ for pane in actual_spawn_panes {
2257
+ let _ = transport.kill_pane(&pane.pane_id);
2258
+ }
2259
+ }
2260
+
2093
2261
  fn write_fake_harness_spawn_argv_event(
2094
2262
  workspace: &Path,
2095
2263
  decision: &RestartedAgent,
@@ -402,6 +402,11 @@ case "$*" in
402
402
  ;;
403
403
  esac
404
404
  case "$*" in
405
+ *"new-window "*|*"new-session "*)
406
+ : > "$spawned_marker"
407
+ printf '%%9288\n'
408
+ exit 0
409
+ ;;
405
410
  *"display-message -p -t $pane #{pane_id}"*)
406
411
  if [ -f "$killed_marker" ]; then
407
412
  echo "can't find pane: $pane" >&2
@@ -410,11 +415,6 @@ case "$*" in
410
415
  printf '%s\n' "$pane"
411
416
  exit 0
412
417
  ;;
413
- *"display-message -p -t $session:alpha #{pane_id}"*)
414
- : > "$spawned_marker"
415
- printf '%%9288\n'
416
- exit 0
417
- ;;
418
418
  *"list-windows -t $session -F #{window_name}"*)
419
419
  exit 0
420
420
  ;;
@@ -873,8 +873,8 @@ fn reset_agent_stop_not_proven_grep_guard_hard_gate_wired() {
873
873
  "reset_agent_at_paths must emit reset_agent.stop_not_proven via the writer"
874
874
  );
875
875
  assert!(
876
- contents.contains("if !agent_is_paused && !stop.stopped"),
877
- "gate must run only when stop.stopped=false (the dangerous case)"
876
+ contents.contains("if !agent_is_paused {"),
877
+ "gate must run after stop for every non-paused reset, including stop.stopped=true residue"
878
878
  );
879
879
  assert!(
880
880
  contents.contains("list_same_role_panes") && contents.contains("is_per_agent_window"),
@@ -744,7 +744,7 @@ impl Transport for BRealTransport {
744
744
  if matches!(self.mode, BRealMode::Misowned) {
745
745
  targets.push(pane_info("team-phaseb", "w2", "%5", 502));
746
746
  }
747
- if matches!(self.mode, BRealMode::Owned) {
747
+ if matches!(self.mode, BRealMode::Owned) && !self.spawns.lock().unwrap().is_empty() {
748
748
  targets.push(pane_info("team-phaseb", "w1", "%10", 501));
749
749
  }
750
750
  Ok(targets)
@@ -553,6 +553,9 @@ fn normalize_value(value: Value, ctx: &mut NormalizeCtx, key: Option<&str>) -> V
553
553
  let sorted = map.into_iter().collect::<BTreeMap<_, _>>();
554
554
  let mut out = Map::new();
555
555
  for (child_key, child) in sorted {
556
+ if matches!(child_key.as_str(), "model_source" | "provider_source") {
557
+ continue;
558
+ }
556
559
  out.insert(
557
560
  child_key.clone(),
558
561
  normalize_value(child, ctx, Some(child_key.as_str())),
@@ -632,6 +632,9 @@ pub enum ResetAgentOutcome {
632
632
  discarded_session_id: Option<SessionId>,
633
633
  session_id: Option<SessionId>,
634
634
  new_session_id: Option<SessionId>,
635
+ capture_state: String,
636
+ reset_proof: String,
637
+ weak_reset_warning: Option<String>,
635
638
  },
636
639
  /// 未传 discard_session → 拒绝(不丢上下文的误用保护)。
637
640
  Refused { reason: ResetRefusal },
@@ -67,6 +67,9 @@ pub(crate) fn reset_agent(
67
67
  discarded_session_id,
68
68
  session_id,
69
69
  new_session_id,
70
+ capture_state,
71
+ reset_proof,
72
+ weak_reset_warning,
70
73
  } => Ok(ToolOk {
71
74
  fields: object_fields(serde_json::json!({
72
75
  "ok": true,
@@ -78,6 +81,13 @@ pub(crate) fn reset_agent(
78
81
  "discarded_session_id": discarded_session_id.as_ref().map(|id| id.as_str()),
79
82
  "session_id": session_id.as_ref().map(|id| id.as_str()),
80
83
  "new_session_id": new_session_id.as_ref().map(|id| id.as_str()),
84
+ "capture_state": match capture_state.as_str() {
85
+ "captured" => "captured",
86
+ "attribution_ambiguous" => "attribution_ambiguous",
87
+ _ => "transcript_missing",
88
+ },
89
+ "reset_proof": if reset_proof == "weak" { "weak" } else { "strong" },
90
+ "weak_reset_warning": weak_reset_warning,
81
91
  })),
82
92
  }),
83
93
  ResetAgentOutcome::Refused { reason } => Ok(ToolOk {
@@ -204,6 +204,7 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
204
204
  report.requests_read = report.requests_read.saturating_add(1);
205
205
  let frame = handle_stdin_line(tools, &line, report)?;
206
206
  if let Some(value) = frame {
207
+ let value = crate::redaction::redact_external_value(&value);
207
208
  serde_json::to_writer(&mut *writer, &value)?;
208
209
  writer.write_all(b"\n")?;
209
210
  writer.flush()?;
@@ -266,7 +267,8 @@ fn rpc_id_from_request(request: &Value) -> RpcId {
266
267
  }
267
268
 
268
269
  fn tool_call_result_value(is_error: bool, body: &Value) -> Value {
269
- let text = json_dumps_default(body);
270
+ let body = crate::redaction::redact_external_value(body);
271
+ let text = json_dumps_default(&body);
270
272
  let mut content = serde_json::Map::new();
271
273
  content.insert("type".to_string(), Value::String("text".to_string()));
272
274
  content.insert("text".to_string(), Value::String(text));