@team-agent/installer 0.5.50 → 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.
Files changed (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -0,0 +1,278 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::lifecycle::*;
6
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
7
+ use crate::model::ids::AgentId;
8
+ use crate::model::permissions::{self, AgentPermissionInput};
9
+ use crate::model::yaml::{self, Value};
10
+ use crate::state::persist::load_runtime_state;
11
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
12
+
13
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
14
+
15
+ use super::*;
16
+
17
+ pub(crate) fn quick_start_tmux_backend(workspace: &Path) -> crate::tmux_backend::TmuxBackend {
18
+ if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
19
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
20
+ } else {
21
+ crate::tmux_backend::TmuxBackend::for_workspace(workspace)
22
+ }
23
+ }
24
+
25
+ pub(crate) fn selected_tmux_socket_source(
26
+ transport: &dyn Transport,
27
+ workspace: &Path,
28
+ ) -> Option<&'static str> {
29
+ let endpoint = transport.tmux_endpoint()?;
30
+ if crate::tmux_backend::socket_name_from_tmux_env().as_deref() == Some(endpoint.as_str()) {
31
+ Some("leader_env")
32
+ } else if endpoint == crate::tmux_backend::socket_name_for_workspace(workspace) {
33
+ Some("workspace")
34
+ } else {
35
+ None
36
+ }
37
+ }
38
+
39
+ pub(crate) fn configure_adaptive_pane_title(
40
+ workspace: &Path,
41
+ transport: &dyn Transport,
42
+ session_name: &SessionName,
43
+ window: &WindowName,
44
+ pane: &PaneId,
45
+ agent_id: &str,
46
+ ) {
47
+ if let Err(error) =
48
+ transport.configure_adaptive_pane_title(session_name, window, pane, agent_id)
49
+ {
50
+ let message = format!("adaptive layout pane title failed for {agent_id}: {error}");
51
+ eprintln!("Warning: {message}");
52
+ if let Err(event_error) = crate::event_log::EventLog::new(workspace).write(
53
+ "adaptive_layout.pane_title_failed",
54
+ serde_json::json!({
55
+ "agent_id": agent_id,
56
+ "session": session_name.as_str(),
57
+ "window": window.as_str(),
58
+ "pane_id": pane.as_str(),
59
+ "warning": message,
60
+ }),
61
+ ) {
62
+ eprintln!(
63
+ "Warning: adaptive_layout.pane_title_failed event write failed: {event_error}"
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ pub(super) fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
70
+ std::fs::canonicalize(workspace).unwrap_or_else(|_| {
71
+ if workspace.is_absolute() {
72
+ workspace.to_path_buf()
73
+ } else {
74
+ std::env::current_dir()
75
+ .unwrap_or_else(|_| PathBuf::from("."))
76
+ .join(workspace)
77
+ }
78
+ })
79
+ }
80
+
81
+ /// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
82
+ /// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
83
+ /// Annotate `state.tmux_endpoint` / `state.tmux_socket` (and `tmux_socket_source`)
84
+ /// from the active transport. Originally only called at `launch_with_transport`
85
+ /// init time; **`0.3.24` opened this to `pub(crate)` so restart/add/fork can keep
86
+ /// the persisted endpoint synchronized with the transport they actually used,
87
+ /// closing the silent socket-drift gap** (single state-save path; no parallel
88
+ /// "annotate after spawn" race with coordinator).
89
+ /// 0.5.x Phase 1d Batch 2: generic runtime-transport annotator.
90
+ ///
91
+ /// Writes `state.transport = { kind, source }` for every backend
92
+ /// (kind = wire string `"tmux"` | `"conpty"`; source = wire string
93
+ /// from `ResolvedTransport.source` when known, else `"unknown"`).
94
+ ///
95
+ /// For tmux, ALSO forwards to `annotate_runtime_tmux_endpoint` so the
96
+ /// existing `tmux_endpoint` / `tmux_socket` / `tmux_socket_source`
97
+ /// fields remain populated byte-equivalent to today's shape.
98
+ ///
99
+ /// The tmux-specific fields are NOT written for ConPTY (CR C-4: no
100
+ /// tmux-only fields under a conpty state; keeps compact status
101
+ /// stable). ConPTY discovery fields (`pipe_name`, `shim_pid`) are
102
+ /// added in Batch 3 when the shim boot path lands; this function just
103
+ /// pins the top-level `transport.kind`/`source`.
104
+ pub fn annotate_runtime_transport(
105
+ state: &mut serde_json::Value,
106
+ transport: &dyn Transport,
107
+ workspace: &Path,
108
+ source: Option<&str>,
109
+ ) {
110
+ use crate::transport::BackendKind;
111
+ let kind_wire = match transport.kind() {
112
+ BackendKind::Tmux => "tmux",
113
+ BackendKind::WezTerm => "wezterm",
114
+ BackendKind::ConPty => "conpty",
115
+ };
116
+ if let Some(obj) = state.as_object_mut() {
117
+ let transport_block = serde_json::json!({
118
+ "kind": kind_wire,
119
+ "source": source.unwrap_or("unknown"),
120
+ });
121
+ obj.insert("transport".to_string(), transport_block);
122
+ }
123
+ // Tmux: preserve the existing tmux endpoint annotation shape.
124
+ if matches!(transport.kind(), BackendKind::Tmux) {
125
+ annotate_runtime_tmux_endpoint(state, transport, workspace);
126
+ }
127
+ }
128
+
129
+ pub(crate) fn annotate_runtime_tmux_endpoint(
130
+ state: &mut serde_json::Value,
131
+ transport: &dyn Transport,
132
+ workspace: &Path,
133
+ ) {
134
+ let Some(endpoint) = transport.tmux_endpoint() else {
135
+ return;
136
+ };
137
+ let endpoint_for_state = if Path::new(&endpoint).is_absolute() || endpoint == "default" {
138
+ endpoint.clone()
139
+ } else if endpoint == crate::tmux_backend::socket_name_for_workspace(workspace) {
140
+ crate::tmux_backend::socket_path_for_workspace(workspace)
141
+ .map(|path| path.to_string_lossy().to_string())
142
+ .unwrap_or_else(|| endpoint.clone())
143
+ } else {
144
+ crate::tmux_backend::socket_path_for_name(&endpoint)
145
+ .map(|path| path.to_string_lossy().to_string())
146
+ .unwrap_or_else(|| endpoint.clone())
147
+ };
148
+ if let Some(obj) = state.as_object_mut() {
149
+ obj.insert(
150
+ "tmux_endpoint".to_string(),
151
+ serde_json::json!(endpoint_for_state),
152
+ );
153
+ obj.insert(
154
+ "tmux_socket".to_string(),
155
+ obj.get("tmux_endpoint")
156
+ .cloned()
157
+ .unwrap_or(serde_json::Value::Null),
158
+ );
159
+ if let Some(source) = selected_tmux_socket_source(transport, workspace) {
160
+ obj.insert("tmux_socket_source".to_string(), serde_json::json!(source));
161
+ }
162
+ }
163
+ }
164
+
165
+ pub(super) fn attach_commands_for_runtime_windows<'a>(
166
+ endpoint: Option<&str>,
167
+ workspace: &Path,
168
+ session_name: &SessionName,
169
+ window_names: impl IntoIterator<Item = &'a str>,
170
+ ) -> Vec<String> {
171
+ let windows = window_names.into_iter().collect::<Vec<_>>();
172
+ let attach = if let Some(endpoint) = endpoint.filter(|endpoint| !endpoint.is_empty()) {
173
+ if Path::new(endpoint).is_absolute() {
174
+ windows
175
+ .iter()
176
+ .map(|window_name| {
177
+ format!(
178
+ "tmux -S {} attach -t {}:{}",
179
+ endpoint,
180
+ session_name.as_str(),
181
+ window_name
182
+ )
183
+ })
184
+ .collect::<Vec<_>>()
185
+ } else {
186
+ crate::tmux_backend::attach_commands_for_windows(
187
+ workspace,
188
+ session_name,
189
+ windows.iter().copied(),
190
+ )
191
+ }
192
+ } else {
193
+ crate::tmux_backend::attach_commands_for_windows(
194
+ workspace,
195
+ session_name,
196
+ windows.iter().copied(),
197
+ )
198
+ };
199
+ attach
200
+ }
201
+
202
+ pub(super) fn started_attach_window_names(started: &[StartedAgent]) -> Vec<String> {
203
+ let mut windows = started
204
+ .iter()
205
+ .map(|started| {
206
+ started
207
+ .layout_window
208
+ .as_ref()
209
+ .map(|window| window.as_str().to_string())
210
+ .unwrap_or_else(|| started.agent_id.as_str().to_string())
211
+ })
212
+ .collect::<Vec<_>>();
213
+ windows.sort();
214
+ windows.dedup();
215
+ windows
216
+ }
217
+
218
+ pub(crate) fn attach_window_names_for_state_agents<'a>(
219
+ state: &serde_json::Value,
220
+ agent_ids: impl IntoIterator<Item = &'a str>,
221
+ ) -> Vec<String> {
222
+ let windows = agent_ids
223
+ .into_iter()
224
+ .map(|agent_id| {
225
+ state
226
+ .get("agents")
227
+ .and_then(serde_json::Value::as_object)
228
+ .and_then(|agents| agents.get(agent_id))
229
+ .and_then(|agent| {
230
+ agent
231
+ .get("layout_window")
232
+ .or_else(|| agent.get("window"))
233
+ .and_then(serde_json::Value::as_str)
234
+ .filter(|window| !window.is_empty())
235
+ })
236
+ .unwrap_or(agent_id)
237
+ .to_string()
238
+ })
239
+ .collect::<Vec<_>>();
240
+ attach_window_names_with_managed_leader(state, windows)
241
+ }
242
+
243
+ pub(super) fn quick_start_attach_window_names(state: &serde_json::Value) -> Vec<String> {
244
+ let windows = state
245
+ .get("agents")
246
+ .and_then(serde_json::Value::as_object)
247
+ .map(|agents| {
248
+ agents
249
+ .iter()
250
+ .filter_map(|(agent_id, agent)| {
251
+ agent
252
+ .get("window")
253
+ .and_then(serde_json::Value::as_str)
254
+ .filter(|window| !window.is_empty())
255
+ .map(str::to_string)
256
+ .or_else(|| Some(agent_id.clone()))
257
+ })
258
+ .collect::<Vec<_>>()
259
+ })
260
+ .unwrap_or_default();
261
+ attach_window_names_with_managed_leader(state, windows)
262
+ }
263
+
264
+ pub(super) fn attach_window_names_with_managed_leader(
265
+ state: &serde_json::Value,
266
+ mut windows: Vec<String>,
267
+ ) -> Vec<String> {
268
+ if state_uses_managed_leader(state) {
269
+ windows.push("leader".to_string());
270
+ }
271
+ windows.sort();
272
+ windows.dedup();
273
+ windows
274
+ }
275
+
276
+ pub(super) fn state_uses_managed_leader(state: &serde_json::Value) -> bool {
277
+ crate::state::projection::state_is_managed_leader(state)
278
+ }
@@ -4,6 +4,22 @@
4
4
  //! Future commits migrate the inline phase fns at launch.rs:2928-2944
5
5
  //! here.
6
6
 
7
+ use std::collections::{BTreeMap, BTreeSet};
8
+ use std::path::{Path, PathBuf};
9
+ use std::process::Command;
10
+
11
+ use crate::lifecycle::*;
12
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
13
+ use crate::model::ids::AgentId;
14
+ use crate::model::permissions::{self, AgentPermissionInput};
15
+ use crate::model::yaml::{self, Value};
16
+ use crate::state::persist::load_runtime_state;
17
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
18
+
19
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
20
+
21
+ use super::*;
22
+
7
23
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
24
  pub enum ReadinessPhase {
9
25
  StartCoordinator,
@@ -30,3 +46,110 @@ mod tests {
30
46
  .starts_with("launch.readiness."));
31
47
  }
32
48
  }
49
+
50
+ pub(super) fn quick_start_worker_readiness(
51
+ workspace: &Path,
52
+ team_key: &str,
53
+ ) -> QuickStartReadiness {
54
+ let Ok(state) = load_runtime_state(workspace) else {
55
+ return QuickStartReadiness::PendingToolLoad;
56
+ };
57
+ let team_state = state
58
+ .get("teams")
59
+ .and_then(serde_json::Value::as_object)
60
+ .and_then(|teams| teams.get(team_key))
61
+ .unwrap_or(&state);
62
+ let Some(agents) = team_state
63
+ .get("agents")
64
+ .and_then(serde_json::Value::as_object)
65
+ else {
66
+ return QuickStartReadiness::PendingToolLoad;
67
+ };
68
+ let all_spawned = !agents.is_empty();
69
+ let leader_receiver_attached = launched_team_receiver_is_attached(workspace, team_key);
70
+ let all_attached_receiver = leader_receiver_attached;
71
+ let mut unhealthy: Vec<String> = agents
72
+ .iter()
73
+ .filter_map(|(id, agent)| {
74
+ let status = agent.get("status").and_then(serde_json::Value::as_str);
75
+ match status {
76
+ Some("running") => None,
77
+ _ => Some(id.clone()),
78
+ }
79
+ })
80
+ .collect();
81
+ if !unhealthy.is_empty() {
82
+ unhealthy.sort();
83
+ unhealthy.dedup();
84
+ QuickStartReadiness::Degraded {
85
+ unhealthy_agents: unhealthy,
86
+ }
87
+ } else {
88
+ let incomplete_agents =
89
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
90
+ let all_resumable_have_session = incomplete_agents.is_empty();
91
+ let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
92
+ QuickStartReadiness::PendingToolLoad
93
+ }
94
+ }
95
+
96
+ pub(super) fn quick_start_session_capture_incomplete_agents(
97
+ workspace: &Path,
98
+ team_key: &str,
99
+ ) -> Vec<String> {
100
+ let Ok(state) = load_runtime_state(workspace) else {
101
+ return Vec::new();
102
+ };
103
+ let team_state = state
104
+ .get("teams")
105
+ .and_then(serde_json::Value::as_object)
106
+ .and_then(|teams| teams.get(team_key))
107
+ .unwrap_or(&state);
108
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state)
109
+ }
110
+
111
+ pub(crate) fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
112
+ let Ok(state) = load_runtime_state(workspace) else {
113
+ return true;
114
+ };
115
+ let team_state = state
116
+ .get("teams")
117
+ .and_then(serde_json::Value::as_object)
118
+ .and_then(|teams| teams.get(team_key))
119
+ .unwrap_or(&state);
120
+ if team_state.get("leader_receiver").is_none() {
121
+ return crate::state::projection::state_is_external_leader(team_state);
122
+ }
123
+ if team_uses_fake_model_harness(team_state) {
124
+ return true;
125
+ }
126
+ leader_receiver_is_attached(team_state)
127
+ }
128
+
129
+ pub(super) fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
130
+ team_state
131
+ .get("agents")
132
+ .and_then(serde_json::Value::as_object)
133
+ .is_some_and(|agents| {
134
+ !agents.is_empty()
135
+ && agents.values().all(|agent| {
136
+ agent.get("model").and_then(serde_json::Value::as_str) == Some("fake")
137
+ })
138
+ })
139
+ }
140
+
141
+ pub(super) fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
142
+ let Some(receiver) = team_state.get("leader_receiver") else {
143
+ return false;
144
+ };
145
+ let status = receiver
146
+ .get("status")
147
+ .and_then(serde_json::Value::as_str)
148
+ .unwrap_or("");
149
+ let pane_id = receiver
150
+ .get("pane_id")
151
+ .and_then(serde_json::Value::as_str)
152
+ .or_else(|| receiver.get("pane").and_then(serde_json::Value::as_str))
153
+ .unwrap_or("");
154
+ status == "attached" && !pane_id.is_empty() && pane_id != "__team_agent_unbound__"
155
+ }