@team-agent/installer 0.3.2 → 0.3.4

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 (82) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +145 -11
  6. package/crates/team-agent/src/cli/emit.rs +287 -53
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +807 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler/tests.rs +2 -2
  16. package/crates/team-agent/src/compiler.rs +16 -6
  17. package/crates/team-agent/src/coordinator/health.rs +89 -20
  18. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  19. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  20. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  21. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  22. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  23. package/crates/team-agent/src/coordinator/types.rs +15 -3
  24. package/crates/team-agent/src/db/schema.rs +37 -2
  25. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  26. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  27. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  28. package/crates/team-agent/src/fake_worker.rs +146 -3
  29. package/crates/team-agent/src/leader/start.rs +121 -23
  30. package/crates/team-agent/src/leader/types.rs +44 -1
  31. package/crates/team-agent/src/lib.rs +3 -0
  32. package/crates/team-agent/src/lifecycle/display.rs +648 -50
  33. package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
  34. package/crates/team-agent/src/lifecycle/mod.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  36. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  37. package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
  38. package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
  40. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  41. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  42. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  45. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
  46. package/crates/team-agent/src/lifecycle/types.rs +23 -0
  47. package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
  48. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  50. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  51. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  52. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  53. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  54. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  55. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  56. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  57. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  58. package/crates/team-agent/src/message_store.rs +21 -4
  59. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  60. package/crates/team-agent/src/messaging/mod.rs +9 -6
  61. package/crates/team-agent/src/messaging/results.rs +153 -16
  62. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  63. package/crates/team-agent/src/messaging/send.rs +35 -3
  64. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  65. package/crates/team-agent/src/messaging/types.rs +11 -3
  66. package/crates/team-agent/src/os_probe.rs +119 -0
  67. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  68. package/crates/team-agent/src/packaging/tests.rs +23 -0
  69. package/crates/team-agent/src/provider/adapter.rs +483 -67
  70. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  71. package/crates/team-agent/src/provider/classify.rs +51 -4
  72. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  73. package/crates/team-agent/src/provider/types.rs +47 -0
  74. package/crates/team-agent/src/session_capture.rs +616 -0
  75. package/crates/team-agent/src/state/persist.rs +57 -0
  76. package/crates/team-agent/src/state/projection.rs +32 -23
  77. package/crates/team-agent/src/state/selector.rs +5 -2
  78. package/crates/team-agent/src/tmux_backend.rs +151 -60
  79. package/crates/team-agent/src/transport/test_support.rs +9 -0
  80. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  81. package/crates/team-agent/src/transport.rs +13 -2
  82. package/package.json +4 -4
@@ -31,7 +31,7 @@
31
31
 
32
32
  use std::path::Path;
33
33
  use std::sync::atomic::{AtomicU64, Ordering};
34
- use std::time::{SystemTime, UNIX_EPOCH};
34
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
35
 
36
36
  use rusqlite::{params, OptionalExtension};
37
37
  use thiserror::Error;
@@ -100,8 +100,25 @@ impl MessageStore {
100
100
  let runtime_dir = workspace.join(".team").join("runtime");
101
101
  std::fs::create_dir_all(&runtime_dir)?;
102
102
  let path = runtime_dir.join("team.db");
103
+ let existed = path.exists();
103
104
  let conn = crate::db::schema::open_db(&path)?;
104
- crate::db::schema::initialize_schema(&conn, Some(&path))?;
105
+ if existed {
106
+ conn.busy_timeout(Duration::from_millis(5))?;
107
+ let version = conn.query_row("pragma user_version", [], |row| row.get::<_, i64>(0));
108
+ conn.busy_timeout(Duration::from_millis(30_000))?;
109
+ match version {
110
+ Ok(version) if version == crate::db::schema::SCHEMA_VERSION => {}
111
+ Ok(_) => crate::db::schema::initialize_schema(&conn, Some(&path))?,
112
+ Err(rusqlite::Error::SqliteFailure(err, _))
113
+ if matches!(
114
+ err.code,
115
+ rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
116
+ ) => {}
117
+ Err(error) => return Err(error.into()),
118
+ }
119
+ } else {
120
+ crate::db::schema::initialize_schema(&conn, Some(&path))?;
121
+ }
105
122
  Ok(Self { path })
106
123
  }
107
124
 
@@ -220,12 +237,12 @@ impl MessageStore {
220
237
  end,
221
238
  updated_at = ?3,
222
239
  delivered_at = case
223
- when ?2 in ('injected', 'visible', 'submitted', 'submitted_unverified', 'delivered')
240
+ when ?2 in ('injected', 'visible', 'submitted', 'delivered')
224
241
  then ?3
225
242
  else delivered_at
226
243
  end,
227
244
  acknowledged_at = case when ?2 = 'acknowledged' then ?3 else acknowledged_at end,
228
- error = coalesce(?4, error)
245
+ error = case when ?2 = 'delivered' then null else coalesce(?4, error) end
229
246
  where message_id = ?1",
230
247
  params![message_id, status, now, error],
231
248
  )?;
@@ -115,18 +115,6 @@ pub fn deliver_pending_message(
115
115
  });
116
116
  }
117
117
  let message = message_for_delivery(store, message_id)?;
118
- if !store.claim_for_delivery(message_id)? {
119
- return Ok(DeliveryOutcome {
120
- ok: false,
121
- status: DeliveryStatus::Refused,
122
- message_status: MessageStatusShadow("target_resolved".to_string()),
123
- message_id: Some(message_id.to_string()),
124
- verification: None,
125
- stage: None,
126
- reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
127
- channel: None,
128
- });
129
- }
130
118
  let Some(message) = message else {
131
119
  return Ok(DeliveryOutcome {
132
120
  ok: false,
@@ -154,6 +142,18 @@ pub fn deliver_pending_message(
154
142
  }
155
143
  _ => state,
156
144
  };
145
+ if !store.claim_for_delivery(message_id)? && message.status != "target_resolved" {
146
+ return Ok(DeliveryOutcome {
147
+ ok: false,
148
+ status: DeliveryStatus::Refused,
149
+ message_status: MessageStatusShadow("target_resolved".to_string()),
150
+ message_id: Some(message_id.to_string()),
151
+ verification: None,
152
+ stage: None,
153
+ reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
154
+ channel: None,
155
+ });
156
+ }
157
157
  if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
158
158
  store.mark(message_id, "failed", Some("leader_not_attached"))?;
159
159
  event_log.write(
@@ -244,7 +244,6 @@ pub fn deliver_pending_message(
244
244
  &message.content,
245
245
  message_id,
246
246
  );
247
- let rendered_len = rendered.len();
248
247
  let inject_report = match transport.inject(
249
248
  &target,
250
249
  &InjectPayload::Text(rendered),
@@ -282,7 +281,7 @@ pub fn deliver_pending_message(
282
281
  return Err(error.into());
283
282
  }
284
283
  };
285
- if !inject_submit_verified(&inject_report, rendered_len, &message.sender, &message.recipient) {
284
+ if !inject_submit_verified(&inject_report) {
286
285
  let reason = format!(
287
286
  "submit_unverified:{}",
288
287
  submit_verification_wire(inject_report.submit_verification)
@@ -340,19 +339,13 @@ pub fn deliver_pending_message(
340
339
  Ok(outcome)
341
340
  }
342
341
 
343
- fn inject_submit_verified(
344
- report: &InjectReport,
345
- payload_len: usize,
346
- sender: &str,
347
- recipient: &str,
348
- ) -> bool {
342
+ fn inject_submit_verified(report: &InjectReport) -> bool {
349
343
  match report.submit_verification {
350
344
  SubmitVerification::SendKeysFailed => false,
345
+ SubmitVerification::PastedContentPromptStillPresentAfterSubmit => false,
351
346
  SubmitVerification::PastedContentPromptAbsentAfterSubmit => true,
352
347
  SubmitVerification::KeySentAfterVisibleToken { .. } => true,
353
- SubmitVerification::EnterSentWithoutPlaceholderCheck => {
354
- recipient == "leader" || matches!(sender, "leader" | "Leader") || payload_len < 80
355
- }
348
+ SubmitVerification::EnterSentWithoutPlaceholderCheck => true,
356
349
  }
357
350
  }
358
351
 
@@ -450,13 +443,60 @@ fn delivery_transport_for_recipient<'a>(
450
443
  if recipient != "leader" {
451
444
  return DeliveryTransport::Borrowed(product_transport);
452
445
  }
446
+ let pane_id = leader_receiver_pane_id(state);
453
447
  let Some(socket) = leader_receiver_tmux_socket(state) else {
448
+ if let Some(pane_id) = pane_id {
449
+ let in_workspace = product_transport
450
+ .list_targets()
451
+ .unwrap_or_default()
452
+ .iter()
453
+ .any(|target| target.pane_id.as_str() == pane_id);
454
+ if !in_workspace {
455
+ let default_backend = crate::tmux_backend::TmuxBackend::new();
456
+ if default_backend
457
+ .list_targets()
458
+ .unwrap_or_default()
459
+ .iter()
460
+ .any(|target| target.pane_id.as_str() == pane_id)
461
+ {
462
+ return DeliveryTransport::Owned(default_backend);
463
+ }
464
+ }
465
+ }
454
466
  return DeliveryTransport::Borrowed(product_transport);
455
467
  };
456
468
  if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
457
469
  DeliveryTransport::Borrowed(product_transport)
458
470
  } else {
459
- DeliveryTransport::Owned(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket))
471
+ let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
472
+ if let Some(pane_id) = pane_id {
473
+ if endpoint_backend
474
+ .list_targets()
475
+ .unwrap_or_default()
476
+ .iter()
477
+ .any(|target| target.pane_id.as_str() == pane_id)
478
+ {
479
+ return DeliveryTransport::Owned(endpoint_backend);
480
+ }
481
+ if product_transport
482
+ .list_targets()
483
+ .unwrap_or_default()
484
+ .iter()
485
+ .any(|target| target.pane_id.as_str() == pane_id)
486
+ {
487
+ return DeliveryTransport::Borrowed(product_transport);
488
+ }
489
+ let default_backend = crate::tmux_backend::TmuxBackend::new();
490
+ if default_backend
491
+ .list_targets()
492
+ .unwrap_or_default()
493
+ .iter()
494
+ .any(|target| target.pane_id.as_str() == pane_id)
495
+ {
496
+ return DeliveryTransport::Owned(default_backend);
497
+ }
498
+ }
499
+ DeliveryTransport::Owned(endpoint_backend)
460
500
  }
461
501
  }
462
502
 
@@ -466,7 +506,7 @@ fn leader_receiver_pane_id_in_state(state: &serde_json::Value) -> Option<&str> {
466
506
  .get(key)
467
507
  .and_then(|r| r.get("pane_id"))
468
508
  .and_then(serde_json::Value::as_str)
469
- .filter(|s| !s.is_empty())
509
+ .filter(|s| !s.is_empty() && *s != "__team_agent_unbound__")
470
510
  })
471
511
  }
472
512
 
@@ -528,7 +568,7 @@ pub fn deliver_pending_messages(
528
568
  let conn = crate::db::schema::open_db(store.db_path())?;
529
569
  let mut stmt = conn.prepare(
530
570
  "select message_id from messages
531
- where status in ('pending', 'accepted')
571
+ where status in ('pending', 'accepted', 'target_resolved')
532
572
  order by created_at, message_id",
533
573
  )?;
534
574
  let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
@@ -577,6 +617,7 @@ struct PendingMessage {
577
617
  content: String,
578
618
  task_id: Option<String>,
579
619
  owner_team_id: Option<String>,
620
+ status: String,
580
621
  }
581
622
 
582
623
  fn message_for_delivery(
@@ -586,7 +627,7 @@ fn message_for_delivery(
586
627
  let conn = crate::db::schema::open_db(store.db_path())?;
587
628
  let message = conn
588
629
  .query_row(
589
- "select sender, recipient, content, task_id, owner_team_id from messages where message_id = ?1",
630
+ "select sender, recipient, content, task_id, owner_team_id, status from messages where message_id = ?1",
590
631
  params![message_id],
591
632
  |row| {
592
633
  Ok(PendingMessage {
@@ -595,6 +636,7 @@ fn message_for_delivery(
595
636
  content: row.get::<_, String>(2)?,
596
637
  task_id: row.get::<_, Option<String>>(3)?,
597
638
  owner_team_id: row.get::<_, Option<String>>(4)?,
639
+ status: row.get::<_, String>(5)?,
598
640
  })
599
641
  },
600
642
  )
@@ -603,10 +645,11 @@ fn message_for_delivery(
603
645
  }
604
646
 
605
647
  /// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
606
- /// actionable Codex startup prompt right now (trust menu or update prompt)" using
607
- /// the SHARED provider/startup_prompt recognizer — no second classifier, no provider
608
- /// API calls. Returns `false` if capture fails so non-Codex providers (or any pane
609
- /// without the trust-menu shape) keep flowing through normal delivery.
648
+ /// actionable provider startup prompt right now (trust menu or update prompt)" using
649
+ /// the SHARED provider/startup_prompt recognizers — no second classifier, no provider
650
+ /// API calls. Returns `false` if capture fails so providers without a startup
651
+ /// recognizer (or any pane without the trust-menu shape) keep flowing through
652
+ /// normal delivery.
610
653
  fn recipient_pane_has_actionable_startup_prompt(
611
654
  transport: &dyn Transport,
612
655
  state: &serde_json::Value,
@@ -620,7 +663,7 @@ fn recipient_pane_has_actionable_startup_prompt(
620
663
  let provider = agent
621
664
  .and_then(|agent| agent.get("provider"))
622
665
  .and_then(serde_json::Value::as_str);
623
- if !matches!(provider, Some("codex")) {
666
+ if !matches!(provider, Some("codex" | "claude" | "claude_code")) {
624
667
  return false;
625
668
  }
626
669
  // step2-retry/scrollback root-cause (rt binary 6c9c6c1c): once the agent's
@@ -643,11 +686,18 @@ fn recipient_pane_has_actionable_startup_prompt(
643
686
  Ok(Ok(captured)) => captured.text,
644
687
  _ => return false,
645
688
  };
646
- matches!(
647
- crate::provider::classify_codex_startup_screen(&captured),
648
- crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
649
- | crate::provider::StartupScreenDecision::SkipUpdatePrompt
650
- )
689
+ match provider {
690
+ Some("codex") => matches!(
691
+ crate::provider::classify_codex_startup_screen(&captured),
692
+ crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
693
+ | crate::provider::StartupScreenDecision::SkipUpdatePrompt
694
+ ),
695
+ Some("claude" | "claude_code") => matches!(
696
+ crate::provider::classify_claude_startup_screen(&captured),
697
+ crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
698
+ ),
699
+ _ => false,
700
+ }
651
701
  }
652
702
 
653
703
  fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
@@ -86,18 +86,21 @@ pub use leader_receiver::{
86
86
  claim_leader_receiver, mirror_peer_message_to_leader, send_to_leader_receiver,
87
87
  };
88
88
  pub use peers::allow_peer_talk;
89
- pub use results::{collect, collect_for_team, collect_results_and_notify_watchers, report_result};
89
+ pub use results::{
90
+ collect, collect_for_team, collect_results_and_notify_watchers, report_result,
91
+ report_result_for_owner_team,
92
+ };
90
93
  pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel, stuck_list};
91
94
  pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
92
95
  pub use send::{apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions};
93
96
  pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
94
97
  pub use types::{
95
98
  ActivityStatus, AgentActivity, AlertSnapshot, AlertSuppression, AlertType, CheckEvidence,
96
- CheckKind, CheckStatus, DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus,
97
- IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery, ProviderSdkCalls,
98
- ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload, TrustRetryPayload,
99
- WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS, TRUST_RETRY_BACKOFF_SECONDS,
100
- TRUST_RETRY_MAX_ATTEMPTS,
99
+ CheckKind, CheckStatus, ContractSuiteCheck, DeliveryOutcome, DeliveryRefusal, DeliveryStage,
100
+ DeliveryStatus, IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
101
+ ProviderSdkCalls, ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
102
+ TrustRetryPayload, WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS,
103
+ TRUST_RETRY_BACKOFF_SECONDS, TRUST_RETRY_MAX_ATTEMPTS,
101
104
  };
102
105
  pub use watchers::{
103
106
  delivered_result_message, format_result_watcher_notification, notify_result_watchers,
@@ -6,6 +6,7 @@ use rusqlite::params;
6
6
 
7
7
  use crate::event_log::EventLog;
8
8
  use crate::message_store::MessageStore;
9
+ use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
9
10
 
10
11
  use super::helpers::{next_result_id, required_str, validate_result_envelope};
11
12
  use super::types::SEND_RETRY_MAX_ATTEMPTS;
@@ -98,11 +99,13 @@ fn collect_scoped(
98
99
  let mut collected = Vec::new();
99
100
  let mut collected_results = Vec::new();
100
101
  let mut invalid_results = Vec::new();
102
+ let mut fatal_invalid_results = 0usize;
101
103
  let mut state_dirty = false;
102
104
  for row in rows {
103
105
  let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
104
106
  Ok(envelope) => envelope,
105
107
  Err(error) => {
108
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
106
109
  record_invalid_result(
107
110
  &conn,
108
111
  &mut invalid_results,
@@ -114,6 +117,7 @@ fn collect_scoped(
114
117
  }
115
118
  };
116
119
  if let Err(error) = validate_result_envelope(&envelope) {
120
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
117
121
  record_invalid_result(
118
122
  &conn,
119
123
  &mut invalid_results,
@@ -128,6 +132,9 @@ fn collect_scoped(
128
132
  } else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id, owner_team_id)? {
129
133
  "message"
130
134
  } else {
135
+ if result_file.is_some() || row.task_id != "manual" {
136
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
137
+ }
131
138
  record_invalid_result(
132
139
  &conn,
133
140
  &mut invalid_results,
@@ -189,7 +196,7 @@ fn collect_scoped(
189
196
  }
190
197
  let counts = result_counts(&conn, owner_team_id)?;
191
198
  Ok(serde_json::json!({
192
- "ok": invalid_results.is_empty(),
199
+ "ok": fatal_invalid_results == 0,
193
200
  "collected": collected,
194
201
  "collected_results": collected_results,
195
202
  "delivered_messages": [],
@@ -478,6 +485,14 @@ fn count_results(
478
485
  pub fn report_result(
479
486
  workspace: &Path,
480
487
  envelope: &serde_json::Value,
488
+ ) -> Result<serde_json::Value, MessagingError> {
489
+ report_result_for_owner_team(workspace, envelope, None)
490
+ }
491
+
492
+ pub fn report_result_for_owner_team(
493
+ workspace: &Path,
494
+ envelope: &serde_json::Value,
495
+ explicit_owner_team: Option<&str>,
481
496
  ) -> Result<serde_json::Value, MessagingError> {
482
497
  validate_result_envelope(envelope)?;
483
498
  let store = MessageStore::open(workspace)?;
@@ -497,7 +512,10 @@ pub fn report_result(
497
512
  let conn = crate::db::schema::open_db(store.db_path())?;
498
513
  let state_for_owner = crate::state::persist::load_runtime_state(workspace)
499
514
  .unwrap_or(serde_json::json!({}));
500
- let owner_team = super::leader_receiver::active_team_key(workspace, &state_for_owner);
515
+ let owner_team = explicit_owner_team
516
+ .filter(|team| !team.is_empty())
517
+ .map(str::to_string)
518
+ .unwrap_or_else(|| super::leader_receiver::active_team_key(workspace, &state_for_owner));
501
519
  let inserted = insert_result_if_absent(
502
520
  &conn,
503
521
  &result_id,
@@ -513,7 +531,7 @@ pub fn report_result(
513
531
  "mcp.report_result_duplicate_ignored",
514
532
  serde_json::json!({
515
533
  "notification_status": "duplicate_ignored",
516
- "owner_team_id": null,
534
+ "owner_team_id": owner_team,
517
535
  "result_id": result_id,
518
536
  }),
519
537
  )?;
@@ -545,7 +563,7 @@ pub fn report_result(
545
563
  // legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
546
564
  // to the caller as "success" while leader actually never saw the result text).
547
565
  let content = format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
548
- let state = crate::state::persist::load_runtime_state(workspace).unwrap_or(serde_json::json!({}));
566
+ let state = report_owner_state(&state_for_owner, &owner_team);
549
567
  let event_log = EventLog::new(workspace);
550
568
  let mut outcome = super::leader_receiver::send_to_leader_receiver(
551
569
  workspace,
@@ -558,19 +576,72 @@ pub fn report_result(
558
576
  Some(&result_id),
559
577
  &event_log,
560
578
  )?;
561
- if matches!(outcome.status, crate::messaging::DeliveryStatus::Queued) {
562
- if let Some(message_id) = outcome.message_id.clone() {
579
+ if let Some(message_id) = outcome.message_id.clone() {
563
580
  let store = MessageStore::open(workspace)?;
564
581
  let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
565
- outcome = super::delivery::deliver_pending_message(
566
- workspace,
567
- &store,
568
- &transport,
569
- &message_id,
570
- &event_log,
571
- &state,
572
- )?;
573
- }
582
+ let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
583
+ .unwrap_or_else(|_| state_for_owner.clone());
584
+ let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
585
+ for attempt in 0..3 {
586
+ let _ = store.mark(&message_id, "accepted", None);
587
+ outcome = super::delivery::deliver_pending_message(
588
+ workspace,
589
+ &store,
590
+ &transport,
591
+ &message_id,
592
+ &event_log,
593
+ &delivery_state,
594
+ )?;
595
+ if outcome.ok {
596
+ break;
597
+ }
598
+ let delivered = super::delivery::deliver_pending_messages(
599
+ workspace,
600
+ &delivery_state,
601
+ &transport,
602
+ &event_log,
603
+ )?;
604
+ if delivered.iter().any(|delivered_id| delivered_id == &message_id) {
605
+ outcome = crate::messaging::DeliveryOutcome {
606
+ ok: true,
607
+ status: crate::messaging::DeliveryStatus::Delivered,
608
+ message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
609
+ message_id: Some(message_id.clone()),
610
+ verification: None,
611
+ stage: None,
612
+ reason: None,
613
+ channel: Some("leader_receiver".to_string()),
614
+ };
615
+ break;
616
+ }
617
+ if attempt < 2 {
618
+ std::thread::sleep(std::time::Duration::from_millis(50));
619
+ }
620
+ }
621
+ match inject_leader_notification_direct(workspace, &delivery_state, &content, &message_id) {
622
+ Ok(()) => {
623
+ store.mark(&message_id, "delivered", None)?;
624
+ outcome = crate::messaging::DeliveryOutcome {
625
+ ok: true,
626
+ status: crate::messaging::DeliveryStatus::Delivered,
627
+ message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
628
+ message_id: Some(message_id),
629
+ verification: None,
630
+ stage: None,
631
+ reason: None,
632
+ channel: Some("leader_receiver".to_string()),
633
+ };
634
+ }
635
+ Err(reason) => {
636
+ event_log.write(
637
+ "leader_receiver.direct_inject_skipped",
638
+ serde_json::json!({
639
+ "message_id": message_id,
640
+ "reason": reason,
641
+ }),
642
+ )?;
643
+ }
644
+ }
574
645
  }
575
646
  let leader_notified = outcome.ok;
576
647
  let notification_status_wire = if outcome.ok {
@@ -590,7 +661,7 @@ pub fn report_result(
590
661
  "notification_channel": channel,
591
662
  "notification_message_id": outcome.message_id,
592
663
  "notification_status": notification_status_wire,
593
- "owner_team_id": null,
664
+ "owner_team_id": owner_team,
594
665
  "result_id": result_id,
595
666
  }),
596
667
  )?;
@@ -624,6 +695,72 @@ pub fn report_result(
624
695
  Ok(serde_json::Value::Object(out))
625
696
  }
626
697
 
698
+ fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
699
+ let mut state = match crate::state::projection::resolve_owner_team_id(state, owner_team)
700
+ .canonical_key()
701
+ {
702
+ Some(team) => crate::state::projection::project_top_level_view(state, team),
703
+ None => state.clone(),
704
+ };
705
+ if let Some(obj) = state.as_object_mut() {
706
+ obj.insert(
707
+ "active_team_key".to_string(),
708
+ serde_json::Value::String(owner_team.to_string()),
709
+ );
710
+ }
711
+ state
712
+ }
713
+
714
+ fn inject_leader_notification_direct(
715
+ workspace: &Path,
716
+ state: &serde_json::Value,
717
+ content: &str,
718
+ message_id: &str,
719
+ ) -> Result<(), String> {
720
+ let Some(pane_id) = state
721
+ .get("leader_receiver")
722
+ .or_else(|| state.get("team_owner"))
723
+ .and_then(|receiver| receiver.get("pane_id"))
724
+ .and_then(serde_json::Value::as_str)
725
+ .filter(|pane| !pane.is_empty() && *pane != "__team_agent_unbound__")
726
+ else {
727
+ return Err("leader_direct_inject_failed:no_bound_pane".to_string());
728
+ };
729
+ let rendered = format!(
730
+ "Team Agent message from leader_receiver:\n\n{content}\n\n[team-agent-token:{message_id}]"
731
+ );
732
+ let target = Target::Pane(PaneId::new(pane_id));
733
+ if let Some(socket) = state
734
+ .get("leader_receiver")
735
+ .and_then(|receiver| receiver.get("tmux_socket"))
736
+ .and_then(serde_json::Value::as_str)
737
+ .filter(|socket| !socket.is_empty())
738
+ {
739
+ let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
740
+ if backend
741
+ .inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
742
+ .is_ok()
743
+ {
744
+ return Ok(());
745
+ }
746
+ }
747
+ let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
748
+ if workspace_backend
749
+ .inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
750
+ .is_ok()
751
+ {
752
+ return Ok(());
753
+ }
754
+ let default_backend = crate::tmux_backend::TmuxBackend::new();
755
+ if default_backend
756
+ .inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
757
+ .is_ok()
758
+ {
759
+ return Ok(());
760
+ }
761
+ Err(format!("leader_direct_inject_failed:pane={pane_id}"))
762
+ }
763
+
627
764
  fn insert_result_if_absent(
628
765
  conn: &rusqlite::Connection,
629
766
  result_id: &str,