@team-agent/installer 0.3.10 → 0.3.11

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.
@@ -2,9 +2,10 @@
2
2
 
3
3
  use std::path::Path;
4
4
 
5
+ use crate::coordinator::{CoordinatorHealthStatus, WorkspacePath};
5
6
  use crate::event_log::EventLog;
6
- use crate::model::ids::{TaskId, TeamKey};
7
7
  use crate::model::enums::PaneLiveness;
8
+ use crate::model::ids::{TaskId, TeamKey};
8
9
  use crate::transport::{PaneId, Transport};
9
10
 
10
11
  use super::helpers::{status_wire, MessageStatusShadow};
@@ -118,7 +119,15 @@ pub fn send_message(
118
119
  MessageTarget::Single(target) => target,
119
120
  MessageTarget::Broadcast => {
120
121
  let recipients = broadcast_recipients(&state, &opts.sender, opts.team.as_ref());
121
- return fanout_send(workspace, &state, &recipients, content, opts, &event_log, "*");
122
+ return fanout_send(
123
+ workspace,
124
+ &state,
125
+ &recipients,
126
+ content,
127
+ opts,
128
+ &event_log,
129
+ "*",
130
+ );
122
131
  }
123
132
  MessageTarget::Fanout(recipients) if recipients.is_empty() => {
124
133
  // swallow batch 3 ②: a failed send carries its reason (Python send error
@@ -135,7 +144,9 @@ pub fn send_message(
135
144
  });
136
145
  }
137
146
  MessageTarget::Fanout(recipients) => {
138
- return fanout_send(workspace, &state, recipients, content, opts, &event_log, "fanout");
147
+ return fanout_send(
148
+ workspace, &state, recipients, content, opts, &event_log, "fanout",
149
+ );
139
150
  }
140
151
  };
141
152
  // send.py:259-261 — a non-leader target that is NOT a known team agent is refused
@@ -170,6 +181,10 @@ pub fn send_message(
170
181
  }
171
182
  }
172
183
  }
184
+ if let Some(outcome) = coordinator_unavailable_outcome(workspace, recipient, opts, &event_log)?
185
+ {
186
+ return Ok(outcome);
187
+ }
173
188
  let store = crate::message_store::MessageStore::open(workspace)?;
174
189
  let task_id = opts.task_id.as_ref().map(|t| t.as_str());
175
190
  let owner_team_id = opts.team.as_ref().map(|t| t.as_str());
@@ -220,9 +235,9 @@ fn task_exists(state: &serde_json::Value, task_id: &TaskId) -> bool {
220
235
  .get("tasks")
221
236
  .and_then(serde_json::Value::as_array)
222
237
  .is_some_and(|tasks| {
223
- tasks
224
- .iter()
225
- .any(|task| task.get("id").and_then(serde_json::Value::as_str) == Some(task_id.as_str()))
238
+ tasks.iter().any(|task| {
239
+ task.get("id").and_then(serde_json::Value::as_str) == Some(task_id.as_str())
240
+ })
226
241
  })
227
242
  }
228
243
 
@@ -259,6 +274,46 @@ fn refused_outcome_with_verification(
259
274
  }
260
275
  }
261
276
 
277
+ fn coordinator_unavailable_outcome(
278
+ workspace: &Path,
279
+ recipient: &str,
280
+ opts: &SendOptions,
281
+ event_log: &EventLog,
282
+ ) -> Result<Option<DeliveryOutcome>, MessagingError> {
283
+ let coordinator_workspace = WorkspacePath::new(workspace.to_path_buf());
284
+ let health = crate::coordinator::coordinator_health(&coordinator_workspace);
285
+ if health.ok || matches!(health.status, CoordinatorHealthStatus::Missing) {
286
+ return Ok(None);
287
+ }
288
+ let warning = format!(
289
+ "coordinator is not running; message was not queued for {recipient}. Run `team-agent diagnose` or restart the team before sending again."
290
+ );
291
+ event_log.write(
292
+ "send.coordinator_unavailable",
293
+ serde_json::json!({
294
+ "recipient": recipient,
295
+ "sender": opts.sender,
296
+ "coordinator_status": health.status,
297
+ "coordinator_pid": health.pid.map(|pid| pid.get()),
298
+ "message_queued": false,
299
+ "warning": warning,
300
+ "coordinator_log": crate::coordinator::coordinator_log_path(&coordinator_workspace)
301
+ .display()
302
+ .to_string(),
303
+ }),
304
+ )?;
305
+ Ok(Some(DeliveryOutcome {
306
+ ok: false,
307
+ status: DeliveryStatus::Degraded,
308
+ message_status: MessageStatusShadow("degraded".to_string()),
309
+ message_id: None,
310
+ verification: Some(warning),
311
+ stage: None,
312
+ reason: Some(DeliveryRefusal::CoordinatorUnavailable),
313
+ channel: Some("coordinator_unavailable".to_string()),
314
+ }))
315
+ }
316
+
262
317
  fn rebind_required_outcome(message_id: Option<String>) -> DeliveryOutcome {
263
318
  rebind_required_outcome_with_verification(
264
319
  message_id,
@@ -291,7 +346,10 @@ fn sender_is_leader(state: &serde_json::Value, sender: &str) -> bool {
291
346
  sender == leader_id || sender == "leader" || sender == "Leader"
292
347
  }
293
348
 
294
- fn backfill_leader_binding_for_delivery_view(state: &mut serde_json::Value, raw_state: &serde_json::Value) {
349
+ fn backfill_leader_binding_for_delivery_view(
350
+ state: &mut serde_json::Value,
351
+ raw_state: &serde_json::Value,
352
+ ) {
295
353
  let Some(obj) = state.as_object_mut() else {
296
354
  return;
297
355
  };
@@ -329,7 +387,9 @@ fn send_owner_gate_refusal(
329
387
  None,
330
388
  )
331
389
  .map_err(|e| MessagingError::Routing(e.to_string()))?;
332
- if let Some(refusal) = crate::state::owner_gate::check_team_owner(state, &caller, false, &LiveLiveness) {
390
+ if let Some(refusal) =
391
+ crate::state::owner_gate::check_team_owner(state, &caller, false, &LiveLiveness)
392
+ {
333
393
  if caller.pane_id.is_empty() {
334
394
  return Ok(Some(refused_outcome(DeliveryRefusal::NoCallerPane)));
335
395
  }
@@ -365,7 +425,8 @@ fn explicit_claim_applied(workspace: &Path, _team_key: &str, _caller_pane: &str)
365
425
  .iter()
366
426
  .rev()
367
427
  .any(|event| {
368
- event.get("event").and_then(serde_json::Value::as_str) == Some("leader_receiver.rebind_applied")
428
+ event.get("event").and_then(serde_json::Value::as_str)
429
+ == Some("leader_receiver.rebind_applied")
369
430
  })
370
431
  }
371
432
 
@@ -514,11 +575,7 @@ fn broadcast_recipients(
514
575
  .and_then(|t| t.get("agents"))
515
576
  .and_then(serde_json::Value::as_object)
516
577
  })
517
- .or_else(|| {
518
- state
519
- .get("agents")
520
- .and_then(serde_json::Value::as_object)
521
- });
578
+ .or_else(|| state.get("agents").and_then(serde_json::Value::as_object));
522
579
  if let Some(agents) = agents_obj {
523
580
  for (agent_id, _) in agents {
524
581
  if agent_id == sender {
@@ -1,6 +1,5 @@
1
1
  use super::*;
2
2
 
3
-
4
3
  // ════════════════════════════════════════════════════════════════════════
5
4
  // GROUP A — serde byte-locks (audit/event wire values; changing one byte
6
5
  // breaks downstream recognizers/event consumers). delivery.py / send.py /
@@ -15,6 +14,7 @@ fn delivery_status_serde_snake_case_byte_locked() {
15
14
  (DeliveryStatus::Queued, "\"queued\""),
16
15
  (DeliveryStatus::Blocked, "\"blocked\""),
17
16
  (DeliveryStatus::Refused, "\"refused\""),
17
+ (DeliveryStatus::Degraded, "\"degraded\""),
18
18
  (DeliveryStatus::RetryScheduled, "\"retry_scheduled\""),
19
19
  (
20
20
  DeliveryStatus::TrustAutoAnswerExhausted,
@@ -22,7 +22,10 @@ fn delivery_status_serde_snake_case_byte_locked() {
22
22
  ),
23
23
  (DeliveryStatus::AlreadyDelivered, "\"already_delivered\""),
24
24
  (DeliveryStatus::FallbackLog, "\"fallback_log\""),
25
- (DeliveryStatus::BroadcastDelivered, "\"broadcast_delivered\""),
25
+ (
26
+ DeliveryStatus::BroadcastDelivered,
27
+ "\"broadcast_delivered\"",
28
+ ),
26
29
  (DeliveryStatus::BroadcastPartial, "\"broadcast_partial\""),
27
30
  (DeliveryStatus::FanoutDelivered, "\"fanout_delivered\""),
28
31
  (DeliveryStatus::FanoutPartial, "\"fanout_partial\""),
@@ -40,16 +43,32 @@ fn delivery_refusal_serde_snake_case_byte_locked() {
40
43
  DeliveryRefusal::HumanConfirmationRequired,
41
44
  "\"human_confirmation_required\"",
42
45
  ),
43
- (DeliveryRefusal::MissingPermissions, "\"missing_permissions\""),
46
+ (
47
+ DeliveryRefusal::MissingPermissions,
48
+ "\"missing_permissions\"",
49
+ ),
44
50
  (DeliveryRefusal::RecipientBusy, "\"recipient_busy\""),
45
51
  (DeliveryRefusal::UnknownRecipient, "\"unknown_recipient\""),
46
- (DeliveryRefusal::TmuxTargetMissing, "\"tmux_target_missing\""),
52
+ (
53
+ DeliveryRefusal::TmuxTargetMissing,
54
+ "\"tmux_target_missing\"",
55
+ ),
47
56
  (
48
57
  DeliveryRefusal::MessageAlreadyClaimed,
49
58
  "\"message_already_claimed\"",
50
59
  ),
51
- (DeliveryRefusal::LeaderNotAttached, "\"leader_not_attached\""),
52
- (DeliveryRefusal::TeamOwnerMismatch, "\"team_owner_mismatch\""),
60
+ (
61
+ DeliveryRefusal::LeaderNotAttached,
62
+ "\"leader_not_attached\"",
63
+ ),
64
+ (
65
+ DeliveryRefusal::CoordinatorUnavailable,
66
+ "\"coordinator_unavailable\"",
67
+ ),
68
+ (
69
+ DeliveryRefusal::TeamOwnerMismatch,
70
+ "\"team_owner_mismatch\"",
71
+ ),
53
72
  (DeliveryRefusal::Ambiguous, "\"ambiguous\""),
54
73
  (
55
74
  DeliveryRefusal::RecipientPaneInNonInputMode,
@@ -354,4 +373,3 @@ fn result_id_from_text_strips_trailing_whitespace() {
354
373
  Some("abc".to_string())
355
374
  );
356
375
  }
357
-