@team-agent/installer 0.3.9 → 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.
Files changed (31) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/send.rs +9 -2
  4. package/crates/team-agent/src/coordinator/backoff.rs +83 -2
  5. package/crates/team-agent/src/coordinator/tests/spine.rs +6 -0
  6. package/crates/team-agent/src/coordinator/tick.rs +410 -168
  7. package/crates/team-agent/src/leader/lease.rs +19 -0
  8. package/crates/team-agent/src/leader/rediscover/tests.rs +12 -0
  9. package/crates/team-agent/src/leader/rediscover.rs +2 -0
  10. package/crates/team-agent/src/lifecycle/launch.rs +35 -0
  11. package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -3
  12. package/crates/team-agent/src/lifecycle/restart/common.rs +75 -0
  13. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +201 -3
  14. package/crates/team-agent/src/lifecycle/restart/selection.rs +51 -14
  15. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/tests/core.rs +89 -15
  17. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +68 -3
  18. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +3 -1
  19. package/crates/team-agent/src/mcp_server/helpers.rs +24 -5
  20. package/crates/team-agent/src/mcp_server/normalize.rs +13 -6
  21. package/crates/team-agent/src/mcp_server/tests/send.rs +310 -212
  22. package/crates/team-agent/src/messaging/delivery.rs +83 -2
  23. package/crates/team-agent/src/messaging/helpers.rs +30 -10
  24. package/crates/team-agent/src/messaging/send.rs +71 -14
  25. package/crates/team-agent/src/messaging/tests/basic.rs +25 -7
  26. package/crates/team-agent/src/messaging/tests/runtime.rs +565 -111
  27. package/crates/team-agent/src/messaging/types.rs +19 -4
  28. package/crates/team-agent/src/provider/approvals/parsing.rs +43 -14
  29. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +12 -9
  30. package/crates/team-agent/src/transport/test_support.rs +12 -1
  31. package/package.json +4 -4
@@ -17,7 +17,7 @@ use crate::transport::{
17
17
  use super::helpers::{message_exists, MessageStatusShadow};
18
18
  use super::{
19
19
  DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError,
20
- PaneWidthQuery, TrustRetryPayload,
20
+ PaneWidthQuery, TrustRetryPayload, SEND_RETRY_MAX_ATTEMPTS,
21
21
  };
22
22
  use crate::state::projection::OwnerTeamResolution;
23
23
 
@@ -286,7 +286,6 @@ pub fn deliver_pending_message(
286
286
  "submit_unverified:{}",
287
287
  submit_verification_wire(inject_report.submit_verification)
288
288
  );
289
- store.mark(message_id, "submitted_unverified", Some(&reason))?;
290
289
  event_log.write(
291
290
  "send.unverified",
292
291
  serde_json::json!({
@@ -296,6 +295,29 @@ pub fn deliver_pending_message(
296
295
  "attempts": inject_report.attempts,
297
296
  }),
298
297
  )?;
298
+ if inject_report.attempts >= u32::from(SEND_RETRY_MAX_ATTEMPTS) {
299
+ store.mark(message_id, "failed", Some("send_unverified_exhausted"))?;
300
+ emit_send_failed_exhausted(
301
+ workspace,
302
+ state,
303
+ event_log,
304
+ message_id,
305
+ &message.recipient,
306
+ inject_report.attempts,
307
+ &reason,
308
+ )?;
309
+ return Ok(DeliveryOutcome {
310
+ ok: false,
311
+ status: DeliveryStatus::Failed,
312
+ message_status: MessageStatusShadow("failed".to_string()),
313
+ message_id: Some(message_id.to_string()),
314
+ verification: Some(reason),
315
+ stage: Some(DeliveryStage::Submit),
316
+ reason: None,
317
+ channel: None,
318
+ });
319
+ }
320
+ store.mark(message_id, "submitted_unverified", Some(&reason))?;
299
321
  return Ok(DeliveryOutcome {
300
322
  ok: false,
301
323
  status: DeliveryStatus::Failed,
@@ -538,6 +560,65 @@ fn leader_receiver_field_in_state<'a>(
538
560
  .filter(|value| !value.is_empty())
539
561
  }
540
562
 
563
+ fn emit_send_failed_exhausted(
564
+ workspace: &Path,
565
+ state: &serde_json::Value,
566
+ event_log: &EventLog,
567
+ message_id: &str,
568
+ recipient: &str,
569
+ attempts: u32,
570
+ verification: &str,
571
+ ) -> Result<(), MessagingError> {
572
+ event_log.write(
573
+ "send.failed",
574
+ serde_json::json!({
575
+ "message_id": message_id,
576
+ "recipient": recipient,
577
+ "attempts": attempts,
578
+ "max_attempts": SEND_RETRY_MAX_ATTEMPTS,
579
+ "reason": "send_unverified_exhausted",
580
+ "verification": verification,
581
+ }),
582
+ )?;
583
+ let content = format!(
584
+ "send.failed\nerror: send to {recipient} remained unverified after {attempts}/{SEND_RETRY_MAX_ATTEMPTS} attempts\naction: inspect the target pane and retry the send\nlog: .team/logs/events.jsonl"
585
+ );
586
+ match crate::messaging::send_to_leader_receiver(
587
+ workspace,
588
+ state,
589
+ "leader",
590
+ &content,
591
+ None,
592
+ "coordinator",
593
+ false,
594
+ Some(&format!("send.failed:{message_id}")),
595
+ event_log,
596
+ ) {
597
+ Ok(outcome) => {
598
+ event_log.write(
599
+ "send.failed_notification",
600
+ serde_json::json!({
601
+ "message_id": message_id,
602
+ "recipient": recipient,
603
+ "leader_notification_status": super::helpers::status_wire(outcome.status),
604
+ "leader_message_id": outcome.message_id,
605
+ }),
606
+ )?;
607
+ }
608
+ Err(error) => {
609
+ event_log.write(
610
+ "send.failed_notification_failed",
611
+ serde_json::json!({
612
+ "message_id": message_id,
613
+ "recipient": recipient,
614
+ "error": error.to_string(),
615
+ }),
616
+ )?;
617
+ }
618
+ }
619
+ Ok(())
620
+ }
621
+
541
622
  fn active_team_entry(state: &serde_json::Value) -> Option<&serde_json::Value> {
542
623
  let team = state.get("active_team_key").and_then(serde_json::Value::as_str)?;
543
624
  state
@@ -29,6 +29,7 @@ pub(crate) fn status_wire(status: DeliveryStatus) -> &'static str {
29
29
  DeliveryStatus::Queued => "queued",
30
30
  DeliveryStatus::Blocked => "blocked",
31
31
  DeliveryStatus::Refused => "refused",
32
+ DeliveryStatus::Degraded => "degraded",
32
33
  DeliveryStatus::RetryScheduled => "retry_scheduled",
33
34
  DeliveryStatus::TrustAutoAnswerExhausted => "trust_auto_answer_exhausted",
34
35
  DeliveryStatus::AlreadyDelivered => "already_delivered",
@@ -40,7 +41,10 @@ pub(crate) fn status_wire(status: DeliveryStatus) -> &'static str {
40
41
  }
41
42
  }
42
43
 
43
- pub(crate) fn message_exists(store: &MessageStore, message_id: &str) -> Result<bool, MessagingError> {
44
+ pub(crate) fn message_exists(
45
+ store: &MessageStore,
46
+ message_id: &str,
47
+ ) -> Result<bool, MessagingError> {
44
48
  let conn = crate::db::schema::open_db(store.db_path())?;
45
49
  let found: Option<String> = conn
46
50
  .query_row(
@@ -65,7 +69,10 @@ pub(crate) fn next_run_id() -> String {
65
69
  id.chars().filter(|c| *c != '_').take(12).collect()
66
70
  }
67
71
 
68
- pub(crate) fn required_str<'a>(value: &'a serde_json::Value, key: &str) -> Result<&'a str, MessagingError> {
72
+ pub(crate) fn required_str<'a>(
73
+ value: &'a serde_json::Value,
74
+ key: &str,
75
+ ) -> Result<&'a str, MessagingError> {
69
76
  value
70
77
  .get(key)
71
78
  .and_then(|v| v.as_str())
@@ -85,7 +92,9 @@ pub(crate) fn validate_result_envelope(envelope: &serde_json::Value) -> Result<(
85
92
  }
86
93
  for key in ["changes", "tests", "risks", "artifacts", "next_actions"] {
87
94
  if !envelope.get(key).is_some_and(serde_json::Value::is_array) {
88
- return Err(MessagingError::Validation(format!("missing required array field: {key}")));
95
+ return Err(MessagingError::Validation(format!(
96
+ "missing required array field: {key}"
97
+ )));
89
98
  }
90
99
  }
91
100
  Ok(())
@@ -122,10 +131,12 @@ pub(crate) fn non_provider_command(command: &str) -> Option<&str> {
122
131
  pub(crate) fn latest_prompt_signal(scrollback: &str) -> Option<AgentActivity> {
123
132
  let lower = scrollback.to_ascii_lowercase();
124
133
  let idle_pos = latest_idle_prompt_pos(scrollback);
125
- let working_pos = ["working", "thinking", "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
126
- .iter()
127
- .filter_map(|needle| lower.rfind(needle))
128
- .max();
134
+ let working_pos = [
135
+ "working", "thinking", "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
136
+ ]
137
+ .iter()
138
+ .filter_map(|needle| lower.rfind(needle))
139
+ .max();
129
140
  match (idle_pos, working_pos) {
130
141
  (Some(i), Some(w)) if i > w => Some(idle_activity()),
131
142
  (Some(_), None) => Some(idle_activity()),
@@ -168,10 +179,19 @@ pub fn fail_leader_delivery(
168
179
  error: Option<&str>,
169
180
  ) -> Result<DeliveryOutcome, MessagingError> {
170
181
  let store = MessageStore::open(workspace)?;
171
- let sender = payload.get("sender").and_then(serde_json::Value::as_str).unwrap_or("system");
172
- let content = payload.get("content").and_then(serde_json::Value::as_str).unwrap_or("");
182
+ let sender = payload
183
+ .get("sender")
184
+ .and_then(serde_json::Value::as_str)
185
+ .unwrap_or("system");
186
+ let content = payload
187
+ .get("content")
188
+ .and_then(serde_json::Value::as_str)
189
+ .unwrap_or("");
173
190
  let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
174
- let message_id = match payload.get("message_id").and_then(serde_json::Value::as_str) {
191
+ let message_id = match payload
192
+ .get("message_id")
193
+ .and_then(serde_json::Value::as_str)
194
+ {
175
195
  Some(existing) => existing.to_string(),
176
196
  None => store.create_message(task_id, sender, "leader", content, None, false, None)?,
177
197
  };
@@ -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
-