@team-agent/installer 0.3.1 → 0.3.3

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 (79) 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 +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  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 +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -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_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,10 +6,12 @@ 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;
12
13
  use crate::model::ids::TaskId;
14
+ use crate::state::projection::OwnerTeamResolution;
13
15
  use super::watchers::retry_result_deliveries;
14
16
  use super::MessagingError;
15
17
 
@@ -19,26 +21,65 @@ pub fn collect(
19
21
  workspace: &Path,
20
22
  result_file: Option<&Path>,
21
23
  ensure_coordinator: bool,
24
+ ) -> Result<serde_json::Value, MessagingError> {
25
+ collect_scoped(workspace, result_file, ensure_coordinator, None)
26
+ }
27
+
28
+ pub fn collect_for_team(
29
+ workspace: &Path,
30
+ result_file: Option<&Path>,
31
+ ensure_coordinator: bool,
32
+ owner_team_id: Option<&str>,
33
+ ) -> Result<serde_json::Value, MessagingError> {
34
+ collect_scoped(workspace, result_file, ensure_coordinator, owner_team_id)
35
+ }
36
+
37
+ fn collect_scoped(
38
+ workspace: &Path,
39
+ result_file: Option<&Path>,
40
+ ensure_coordinator: bool,
41
+ owner_team_id: Option<&str>,
22
42
  ) -> Result<serde_json::Value, MessagingError> {
23
43
  let _ = ensure_coordinator;
24
44
  let paths = collect_paths(workspace)?;
25
- let spec_path = paths.spec_workspace.join("team.spec.yaml");
45
+ let log = EventLog::new(&paths.run_workspace);
46
+ let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
47
+ Some(team) => Some(resolve_owner_team_for_read(&paths.run_workspace, team, Some(&log))?),
48
+ None => None,
49
+ };
50
+ let owner_team_id = resolved_owner_team_id.as_deref();
51
+ let mut state = match owner_team_id {
52
+ Some(team) => crate::state::projection::select_runtime_state(&paths.run_workspace, Some(team))?,
53
+ None => crate::state::persist::load_runtime_state(&paths.run_workspace)?,
54
+ };
55
+ let spec_workspace = owner_team_id
56
+ .and_then(|_| state_spec_workspace_from_value(&state))
57
+ .unwrap_or_else(|| paths.spec_workspace.clone());
58
+ let spec_path = spec_workspace.join("team.spec.yaml");
26
59
  if !spec_path.exists() {
27
60
  return Err(MessagingError::Validation(format!("Cannot read {}", spec_path.display())));
28
61
  }
29
62
  let store = MessageStore::open(&paths.run_workspace)?;
30
63
  let conn = crate::db::schema::open_db(store.db_path())?;
31
64
  if let Some(path) = result_file {
32
- ingest_result_file(&conn, path)?;
65
+ ingest_result_file(&conn, path, owner_team_id)?;
33
66
  }
34
- let mut stmt = conn.prepare(
35
- "select result_id, task_id, agent_id, envelope, status, created_at
36
- from results
37
- where status not in ('collected', 'invalid')
38
- order by created_at, result_id",
39
- )?;
40
- let rows = stmt
41
- .query_map([], |row| {
67
+ let sql = match owner_team_id {
68
+ Some(_) => {
69
+ "select result_id, task_id, agent_id, envelope, status, created_at
70
+ from results
71
+ where status not in ('collected', 'invalid') and owner_team_id = ?1
72
+ order by created_at, result_id"
73
+ }
74
+ None => {
75
+ "select result_id, task_id, agent_id, envelope, status, created_at
76
+ from results
77
+ where status not in ('collected', 'invalid')
78
+ order by created_at, result_id"
79
+ }
80
+ };
81
+ let mut stmt = conn.prepare(sql)?;
82
+ let row_mapper = |row: &rusqlite::Row<'_>| {
42
83
  Ok(StoredResult {
43
84
  result_id: row.get(0)?,
44
85
  task_id: row.get(1)?,
@@ -47,20 +88,24 @@ pub fn collect(
47
88
  status: row.get(4)?,
48
89
  created_at: row.get(5)?,
49
90
  })
50
- })?
91
+ };
92
+ let rows = match owner_team_id {
93
+ Some(team) => stmt.query_map(params![team], row_mapper),
94
+ None => stmt.query_map([], row_mapper),
95
+ }?
51
96
  .collect::<Result<Vec<_>, _>>()?;
52
97
  drop(stmt);
53
98
 
54
- let mut state = crate::state::persist::load_runtime_state(&paths.run_workspace)?;
55
99
  let mut collected = Vec::new();
56
100
  let mut collected_results = Vec::new();
57
101
  let mut invalid_results = Vec::new();
102
+ let mut fatal_invalid_results = 0usize;
58
103
  let mut state_dirty = false;
59
- let log = EventLog::new(&paths.run_workspace);
60
104
  for row in rows {
61
105
  let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
62
106
  Ok(envelope) => envelope,
63
107
  Err(error) => {
108
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
64
109
  record_invalid_result(
65
110
  &conn,
66
111
  &mut invalid_results,
@@ -72,6 +117,7 @@ pub fn collect(
72
117
  }
73
118
  };
74
119
  if let Err(error) = validate_result_envelope(&envelope) {
120
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
75
121
  record_invalid_result(
76
122
  &conn,
77
123
  &mut invalid_results,
@@ -83,9 +129,12 @@ pub fn collect(
83
129
  }
84
130
  let scope = if task_exists(&state, &row.task_id) {
85
131
  "task"
86
- } else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id)? {
132
+ } else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id, owner_team_id)? {
87
133
  "message"
88
134
  } else {
135
+ if result_file.is_some() || row.task_id != "manual" {
136
+ fatal_invalid_results = fatal_invalid_results.saturating_add(1);
137
+ }
89
138
  record_invalid_result(
90
139
  &conn,
91
140
  &mut invalid_results,
@@ -95,10 +144,20 @@ pub fn collect(
95
144
  )?;
96
145
  continue;
97
146
  };
98
- conn.execute(
99
- "update results set status = 'collected' where result_id = ?1",
100
- params![row.result_id.as_str()],
101
- )?;
147
+ match owner_team_id {
148
+ Some(team) => {
149
+ conn.execute(
150
+ "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2",
151
+ params![row.result_id.as_str(), team],
152
+ )?;
153
+ }
154
+ None => {
155
+ conn.execute(
156
+ "update results set status = 'collected' where result_id = ?1",
157
+ params![row.result_id.as_str()],
158
+ )?;
159
+ }
160
+ }
102
161
  if scope == "task" {
103
162
  mark_task_done(&mut state, &row.task_id, &row.result_id);
104
163
  state_dirty = true;
@@ -129,17 +188,21 @@ pub fn collect(
129
188
  collected_results.push(summary);
130
189
  }
131
190
  if state_dirty {
132
- crate::state::persist::save_runtime_state(&paths.run_workspace, &state)?;
191
+ if owner_team_id.is_some() {
192
+ crate::state::projection::save_team_scoped_state(&paths.run_workspace, &state)?;
193
+ } else {
194
+ crate::state::persist::save_runtime_state(&paths.run_workspace, &state)?;
195
+ }
133
196
  }
134
- let counts = result_counts(&conn)?;
197
+ let counts = result_counts(&conn, owner_team_id)?;
135
198
  Ok(serde_json::json!({
136
- "ok": invalid_results.is_empty(),
199
+ "ok": fatal_invalid_results == 0,
137
200
  "collected": collected,
138
201
  "collected_results": collected_results,
139
202
  "delivered_messages": [],
140
203
  "invalid_results": invalid_results,
141
204
  "results": counts,
142
- "state_file": paths.spec_workspace.join("team_state.md").to_string_lossy().to_string(),
205
+ "state_file": spec_workspace.join("team_state.md").to_string_lossy().to_string(),
143
206
  "coordinator": {
144
207
  "ok": false,
145
208
  "status": "not_required",
@@ -147,12 +210,48 @@ pub fn collect(
147
210
  }))
148
211
  }
149
212
 
213
+ fn resolve_owner_team_for_read(
214
+ workspace: &Path,
215
+ requested: &str,
216
+ event_log: Option<&EventLog>,
217
+ ) -> Result<String, MessagingError> {
218
+ let state = crate::state::persist::load_runtime_state(workspace)?;
219
+ match crate::state::projection::resolve_owner_team_id(&state, requested) {
220
+ OwnerTeamResolution::Canonical(canonical) => Ok(canonical),
221
+ OwnerTeamResolution::LegacyAlias { requested, canonical } => {
222
+ crate::messaging::delivery::normalize_owner_team_id_rows(
223
+ workspace,
224
+ &requested,
225
+ &canonical,
226
+ None,
227
+ event_log,
228
+ )?;
229
+ Ok(canonical)
230
+ }
231
+ OwnerTeamResolution::Unresolved { requested } => {
232
+ Err(MessagingError::Routing(format!("owner_team_unresolved: {requested}")))
233
+ }
234
+ OwnerTeamResolution::Ambiguous { requested, matches } => {
235
+ Err(MessagingError::Routing(format!(
236
+ "owner_team_ambiguous: {requested} matches {}",
237
+ matches.join(",")
238
+ )))
239
+ }
240
+ }
241
+ }
242
+
150
243
  struct CollectPaths {
151
244
  run_workspace: PathBuf,
152
245
  spec_workspace: PathBuf,
153
246
  }
154
247
 
155
248
  fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
249
+ if collect_input_has_no_local_team_context(workspace) {
250
+ return Ok(CollectPaths {
251
+ run_workspace: workspace.to_path_buf(),
252
+ spec_workspace: workspace.to_path_buf(),
253
+ });
254
+ }
156
255
  let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
157
256
  .map_err(|e| MessagingError::Routing(e.to_string()))?;
158
257
  let spec_workspace = if workspace.join("team.spec.yaml").exists() {
@@ -168,8 +267,24 @@ fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
168
267
  })
169
268
  }
170
269
 
270
+ fn collect_input_has_no_local_team_context(workspace: &Path) -> bool {
271
+ !workspace.join("team.spec.yaml").exists()
272
+ && !workspace.join(".team").exists()
273
+ && !crate::state::persist::runtime_state_path(workspace).exists()
274
+ && workspace.file_name().and_then(|s| s.to_str()) != Some(".team")
275
+ && workspace
276
+ .parent()
277
+ .and_then(|p| p.file_name())
278
+ .and_then(|s| s.to_str())
279
+ != Some(".team")
280
+ }
281
+
171
282
  fn state_spec_workspace(run_workspace: &Path) -> Option<PathBuf> {
172
283
  let state = crate::state::persist::load_runtime_state(run_workspace).ok()?;
284
+ state_spec_workspace_from_value(&state)
285
+ }
286
+
287
+ fn state_spec_workspace_from_value(state: &serde_json::Value) -> Option<PathBuf> {
173
288
  if let Some(spec_path) = state.get("spec_path").and_then(serde_json::Value::as_str) {
174
289
  return PathBuf::from(spec_path).parent().map(Path::to_path_buf);
175
290
  }
@@ -200,7 +315,11 @@ fn record_invalid_result(
200
315
  Ok(())
201
316
  }
202
317
 
203
- fn ingest_result_file(conn: &rusqlite::Connection, path: &Path) -> Result<(), MessagingError> {
318
+ fn ingest_result_file(
319
+ conn: &rusqlite::Connection,
320
+ path: &Path,
321
+ owner_team_id: Option<&str>,
322
+ ) -> Result<(), MessagingError> {
204
323
  let raw = std::fs::read_to_string(path)?;
205
324
  let mut envelope: serde_json::Value = serde_json::from_str(&raw)?;
206
325
  validate_result_envelope(&envelope)?;
@@ -226,7 +345,7 @@ fn ingest_result_file(conn: &rusqlite::Connection, path: &Path) -> Result<(), Me
226
345
  agent_id,
227
346
  &envelope.to_string(),
228
347
  status,
229
- None,
348
+ owner_team_id,
230
349
  )?;
231
350
  Ok(())
232
351
  }
@@ -273,39 +392,55 @@ fn is_message_scoped_result(
273
392
  conn: &rusqlite::Connection,
274
393
  task_id: &str,
275
394
  agent_id: &str,
395
+ owner_team_id: Option<&str>,
276
396
  ) -> Result<bool, MessagingError> {
277
397
  if !task_id.starts_with("msg_") {
278
398
  return Ok(false);
279
399
  }
280
- let count: i64 = conn.query_row(
281
- "select count(*) from messages where message_id = ?1 and recipient = ?2",
282
- params![task_id, agent_id],
283
- |row| row.get(0),
284
- )?;
400
+ let count: i64 = match owner_team_id {
401
+ Some(team) => conn.query_row(
402
+ "select count(*) from messages where message_id = ?1 and recipient = ?2 and owner_team_id = ?3",
403
+ params![task_id, agent_id, team],
404
+ |row| row.get(0),
405
+ )?,
406
+ None => conn.query_row(
407
+ "select count(*) from messages where message_id = ?1 and recipient = ?2",
408
+ params![task_id, agent_id],
409
+ |row| row.get(0),
410
+ )?,
411
+ };
285
412
  Ok(count > 0)
286
413
  }
287
414
 
288
- fn result_counts(conn: &rusqlite::Connection) -> Result<serde_json::Value, MessagingError> {
289
- let total: i64 = conn.query_row("select count(*) from results", [], |row| row.get(0))?;
290
- let collected: i64 = conn.query_row(
291
- "select count(*) from results where status = 'collected'",
292
- [],
293
- |row| row.get(0),
294
- )?;
295
- let invalid: i64 = conn.query_row(
296
- "select count(*) from results where status = 'invalid'",
297
- [],
298
- |row| row.get(0),
299
- )?;
415
+ fn result_counts(
416
+ conn: &rusqlite::Connection,
417
+ owner_team_id: Option<&str>,
418
+ ) -> Result<serde_json::Value, MessagingError> {
419
+ let total: i64 = count_results(conn, owner_team_id, None)?;
420
+ let collected: i64 = count_results(conn, owner_team_id, Some("collected"))?;
421
+ let invalid: i64 = count_results(conn, owner_team_id, Some("invalid"))?;
300
422
  let uncollected = total - collected - invalid;
301
423
  let mut by_status = serde_json::Map::new();
302
- let mut stmt = conn.prepare(
303
- "select status, count(*) from results
304
- where status not in ('collected', 'invalid')
305
- group by status
306
- order by status",
307
- )?;
308
- let rows = stmt.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)))?;
424
+ let sql = match owner_team_id {
425
+ Some(_) => {
426
+ "select status, count(*) from results
427
+ where status not in ('collected', 'invalid') and owner_team_id = ?1
428
+ group by status
429
+ order by status"
430
+ }
431
+ None => {
432
+ "select status, count(*) from results
433
+ where status not in ('collected', 'invalid')
434
+ group by status
435
+ order by status"
436
+ }
437
+ };
438
+ let mut stmt = conn.prepare(sql)?;
439
+ let row_mapper = |row: &rusqlite::Row<'_>| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?));
440
+ let rows = match owner_team_id {
441
+ Some(team) => stmt.query_map(params![team], row_mapper),
442
+ None => stmt.query_map([], row_mapper),
443
+ }?;
309
444
  for row in rows {
310
445
  let (status, count) = row?;
311
446
  by_status.insert(status, serde_json::Value::Number(count.into()));
@@ -319,12 +454,45 @@ fn result_counts(conn: &rusqlite::Connection) -> Result<serde_json::Value, Messa
319
454
  }))
320
455
  }
321
456
 
457
+ fn count_results(
458
+ conn: &rusqlite::Connection,
459
+ owner_team_id: Option<&str>,
460
+ status: Option<&str>,
461
+ ) -> Result<i64, MessagingError> {
462
+ match (owner_team_id, status) {
463
+ (Some(team), Some(status)) => Ok(conn.query_row(
464
+ "select count(*) from results where owner_team_id = ?1 and status = ?2",
465
+ params![team, status],
466
+ |row| row.get(0),
467
+ )?),
468
+ (Some(team), None) => Ok(conn.query_row(
469
+ "select count(*) from results where owner_team_id = ?1",
470
+ params![team],
471
+ |row| row.get(0),
472
+ )?),
473
+ (None, Some(status)) => Ok(conn.query_row(
474
+ "select count(*) from results where status = ?1",
475
+ params![status],
476
+ |row| row.get(0),
477
+ )?),
478
+ (None, None) => Ok(conn.query_row("select count(*) from results", [], |row| row.get(0))?),
479
+ }
480
+ }
481
+
322
482
  /// `report_result` (`results.py:191`):worker 报结果 —— 校验 envelope、存 result、ack 任务消息、
323
483
  /// **排队** send 事件通知 leader、推进 orchestrator (软依赖,失败仅记 `orchestrator.advance_skipped`)。
324
484
  /// MCP `report_result` 工具调。
325
485
  pub fn report_result(
326
486
  workspace: &Path,
327
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>,
328
496
  ) -> Result<serde_json::Value, MessagingError> {
329
497
  validate_result_envelope(envelope)?;
330
498
  let store = MessageStore::open(workspace)?;
@@ -344,7 +512,10 @@ pub fn report_result(
344
512
  let conn = crate::db::schema::open_db(store.db_path())?;
345
513
  let state_for_owner = crate::state::persist::load_runtime_state(workspace)
346
514
  .unwrap_or(serde_json::json!({}));
347
- 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));
348
519
  let inserted = insert_result_if_absent(
349
520
  &conn,
350
521
  &result_id,
@@ -360,7 +531,7 @@ pub fn report_result(
360
531
  "mcp.report_result_duplicate_ignored",
361
532
  serde_json::json!({
362
533
  "notification_status": "duplicate_ignored",
363
- "owner_team_id": null,
534
+ "owner_team_id": owner_team,
364
535
  "result_id": result_id,
365
536
  }),
366
537
  )?;
@@ -392,7 +563,7 @@ pub fn report_result(
392
563
  // legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
393
564
  // to the caller as "success" while leader actually never saw the result text).
394
565
  let content = format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
395
- 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);
396
567
  let event_log = EventLog::new(workspace);
397
568
  let mut outcome = super::leader_receiver::send_to_leader_receiver(
398
569
  workspace,
@@ -405,19 +576,72 @@ pub fn report_result(
405
576
  Some(&result_id),
406
577
  &event_log,
407
578
  )?;
408
- if matches!(outcome.status, crate::messaging::DeliveryStatus::Queued) {
409
- if let Some(message_id) = outcome.message_id.clone() {
579
+ if let Some(message_id) = outcome.message_id.clone() {
410
580
  let store = MessageStore::open(workspace)?;
411
581
  let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
412
- outcome = super::delivery::deliver_pending_message(
413
- workspace,
414
- &store,
415
- &transport,
416
- &message_id,
417
- &event_log,
418
- &state,
419
- )?;
420
- }
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
+ }
421
645
  }
422
646
  let leader_notified = outcome.ok;
423
647
  let notification_status_wire = if outcome.ok {
@@ -437,7 +661,7 @@ pub fn report_result(
437
661
  "notification_channel": channel,
438
662
  "notification_message_id": outcome.message_id,
439
663
  "notification_status": notification_status_wire,
440
- "owner_team_id": null,
664
+ "owner_team_id": owner_team,
441
665
  "result_id": result_id,
442
666
  }),
443
667
  )?;
@@ -471,6 +695,72 @@ pub fn report_result(
471
695
  Ok(serde_json::Value::Object(out))
472
696
  }
473
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
+
474
764
  fn insert_result_if_absent(
475
765
  conn: &rusqlite::Connection,
476
766
  result_id: &str,