@team-agent/installer 0.3.14 → 0.3.16
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/leader.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +255 -37
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/status.rs +5 -1
- package/crates/team-agent/src/cli/status_port.rs +24 -0
- package/crates/team-agent/src/cli/tests/base.rs +21 -3
- package/crates/team-agent/src/cli/tests/divergence.rs +6 -6
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +285 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +25 -0
- package/crates/team-agent/src/cli/types.rs +32 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +272 -8
- package/crates/team-agent/src/leader/tests/basics.rs +1 -0
- package/crates/team-agent/src/leader/tests/identity.rs +121 -25
- package/crates/team-agent/src/leader/types.rs +9 -0
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +631 -70
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +79 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +731 -8
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +85 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +27 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -6,14 +6,13 @@ 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};
|
|
10
9
|
|
|
11
10
|
use super::helpers::{next_result_id, required_str, validate_result_envelope};
|
|
12
11
|
use super::types::SEND_RETRY_MAX_ATTEMPTS;
|
|
13
|
-
use crate::model::ids::TaskId;
|
|
14
|
-
use crate::state::projection::OwnerTeamResolution;
|
|
15
12
|
use super::watchers::retry_result_deliveries;
|
|
16
13
|
use super::MessagingError;
|
|
14
|
+
use crate::model::ids::TaskId;
|
|
15
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
17
16
|
|
|
18
17
|
/// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
|
|
19
18
|
/// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
|
|
@@ -43,12 +42,18 @@ fn collect_scoped(
|
|
|
43
42
|
let paths = collect_paths(workspace)?;
|
|
44
43
|
let log = EventLog::new(&paths.run_workspace);
|
|
45
44
|
let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
|
|
46
|
-
Some(team) => Some(resolve_owner_team_for_read(
|
|
45
|
+
Some(team) => Some(resolve_owner_team_for_read(
|
|
46
|
+
&paths.run_workspace,
|
|
47
|
+
team,
|
|
48
|
+
Some(&log),
|
|
49
|
+
)?),
|
|
47
50
|
None => None,
|
|
48
51
|
};
|
|
49
52
|
let owner_team_id = resolved_owner_team_id.as_deref();
|
|
50
53
|
let mut state = match owner_team_id {
|
|
51
|
-
Some(team) =>
|
|
54
|
+
Some(team) => {
|
|
55
|
+
crate::state::projection::select_runtime_state(&paths.run_workspace, Some(team))?
|
|
56
|
+
}
|
|
52
57
|
None => crate::state::persist::load_runtime_state(&paths.run_workspace)?,
|
|
53
58
|
};
|
|
54
59
|
let spec_workspace = owner_team_id
|
|
@@ -56,7 +61,10 @@ fn collect_scoped(
|
|
|
56
61
|
.unwrap_or_else(|| paths.spec_workspace.clone());
|
|
57
62
|
let spec_path = spec_workspace.join("team.spec.yaml");
|
|
58
63
|
if !spec_path.exists() {
|
|
59
|
-
return Err(MessagingError::Validation(format!(
|
|
64
|
+
return Err(MessagingError::Validation(format!(
|
|
65
|
+
"Cannot read {}",
|
|
66
|
+
spec_path.display()
|
|
67
|
+
)));
|
|
60
68
|
}
|
|
61
69
|
let store = MessageStore::open(&paths.run_workspace)?;
|
|
62
70
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
@@ -79,20 +87,20 @@ fn collect_scoped(
|
|
|
79
87
|
};
|
|
80
88
|
let mut stmt = conn.prepare(sql)?;
|
|
81
89
|
let row_mapper = |row: &rusqlite::Row<'_>| {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
Ok(StoredResult {
|
|
91
|
+
result_id: row.get(0)?,
|
|
92
|
+
task_id: row.get(1)?,
|
|
93
|
+
agent_id: row.get(2)?,
|
|
94
|
+
envelope: row.get(3)?,
|
|
95
|
+
status: row.get(4)?,
|
|
96
|
+
created_at: row.get(5)?,
|
|
97
|
+
})
|
|
98
|
+
};
|
|
91
99
|
let rows = match owner_team_id {
|
|
92
100
|
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
93
101
|
None => stmt.query_map([], row_mapper),
|
|
94
102
|
}?
|
|
95
|
-
|
|
103
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
96
104
|
drop(stmt);
|
|
97
105
|
|
|
98
106
|
let mut collected = Vec::new();
|
|
@@ -225,7 +233,9 @@ fn ensure_coordinator_after_collect(
|
|
|
225
233
|
let workspace_path = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
226
234
|
let coordinator = match crate::coordinator::start_coordinator(&workspace_path) {
|
|
227
235
|
Ok(report) => start_report_value(&report),
|
|
228
|
-
Err(e) =>
|
|
236
|
+
Err(e) => {
|
|
237
|
+
serde_json::json!({"ok": false, "status": "start_failed", "error": e.to_string()})
|
|
238
|
+
}
|
|
229
239
|
};
|
|
230
240
|
let _ = log.write(
|
|
231
241
|
"collect.coordinator_checked",
|
|
@@ -289,19 +299,18 @@ fn resolve_owner_team_for_read(
|
|
|
289
299
|
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
290
300
|
match crate::state::projection::resolve_owner_team_id(&state, requested) {
|
|
291
301
|
OwnerTeamResolution::Canonical(canonical) => Ok(canonical),
|
|
292
|
-
OwnerTeamResolution::LegacyAlias {
|
|
302
|
+
OwnerTeamResolution::LegacyAlias {
|
|
303
|
+
requested,
|
|
304
|
+
canonical,
|
|
305
|
+
} => {
|
|
293
306
|
crate::messaging::delivery::normalize_owner_team_id_rows(
|
|
294
|
-
workspace,
|
|
295
|
-
&requested,
|
|
296
|
-
&canonical,
|
|
297
|
-
None,
|
|
298
|
-
event_log,
|
|
307
|
+
workspace, &requested, &canonical, None, event_log,
|
|
299
308
|
)?;
|
|
300
309
|
Ok(canonical)
|
|
301
310
|
}
|
|
302
|
-
OwnerTeamResolution::Unresolved { requested } =>
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
OwnerTeamResolution::Unresolved { requested } => Err(MessagingError::Routing(format!(
|
|
312
|
+
"owner_team_unresolved: {requested}"
|
|
313
|
+
))),
|
|
305
314
|
OwnerTeamResolution::Ambiguous { requested, matches } => {
|
|
306
315
|
Err(MessagingError::Routing(format!(
|
|
307
316
|
"owner_team_ambiguous: {requested} matches {}",
|
|
@@ -431,7 +440,10 @@ struct StoredResult {
|
|
|
431
440
|
}
|
|
432
441
|
|
|
433
442
|
fn mark_task_done(state: &mut serde_json::Value, task_id: &str, result_id: &str) {
|
|
434
|
-
let Some(tasks) = state
|
|
443
|
+
let Some(tasks) = state
|
|
444
|
+
.get_mut("tasks")
|
|
445
|
+
.and_then(serde_json::Value::as_array_mut)
|
|
446
|
+
else {
|
|
435
447
|
return;
|
|
436
448
|
};
|
|
437
449
|
for task in tasks {
|
|
@@ -439,7 +451,10 @@ fn mark_task_done(state: &mut serde_json::Value, task_id: &str, result_id: &str)
|
|
|
439
451
|
continue;
|
|
440
452
|
}
|
|
441
453
|
if let Some(obj) = task.as_object_mut() {
|
|
442
|
-
obj.insert(
|
|
454
|
+
obj.insert(
|
|
455
|
+
"status".to_string(),
|
|
456
|
+
serde_json::Value::String("done".to_string()),
|
|
457
|
+
);
|
|
443
458
|
obj.insert(
|
|
444
459
|
"accepted_result_id".to_string(),
|
|
445
460
|
serde_json::Value::String(result_id.to_string()),
|
|
@@ -507,7 +522,8 @@ fn result_counts(
|
|
|
507
522
|
}
|
|
508
523
|
};
|
|
509
524
|
let mut stmt = conn.prepare(sql)?;
|
|
510
|
-
let row_mapper =
|
|
525
|
+
let row_mapper =
|
|
526
|
+
|row: &rusqlite::Row<'_>| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?));
|
|
511
527
|
let rows = match owner_team_id {
|
|
512
528
|
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
513
529
|
None => stmt.query_map([], row_mapper),
|
|
@@ -564,6 +580,29 @@ pub fn report_result_for_owner_team(
|
|
|
564
580
|
workspace: &Path,
|
|
565
581
|
envelope: &serde_json::Value,
|
|
566
582
|
explicit_owner_team: Option<&str>,
|
|
583
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
584
|
+
report_result_for_owner_team_inner(workspace, envelope, explicit_owner_team, None)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
pub fn report_result_for_owner_team_with_primary_error(
|
|
588
|
+
workspace: &Path,
|
|
589
|
+
envelope: &serde_json::Value,
|
|
590
|
+
explicit_owner_team: Option<&str>,
|
|
591
|
+
fallback_primary_error: Option<&str>,
|
|
592
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
593
|
+
report_result_for_owner_team_inner(
|
|
594
|
+
workspace,
|
|
595
|
+
envelope,
|
|
596
|
+
explicit_owner_team,
|
|
597
|
+
fallback_primary_error,
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
fn report_result_for_owner_team_inner(
|
|
602
|
+
workspace: &Path,
|
|
603
|
+
envelope: &serde_json::Value,
|
|
604
|
+
explicit_owner_team: Option<&str>,
|
|
605
|
+
fallback_primary_error: Option<&str>,
|
|
567
606
|
) -> Result<serde_json::Value, MessagingError> {
|
|
568
607
|
validate_result_envelope(envelope)?;
|
|
569
608
|
let store = MessageStore::open(workspace)?;
|
|
@@ -577,12 +616,15 @@ pub fn report_result_for_owner_team(
|
|
|
577
616
|
let mut stored = envelope.clone();
|
|
578
617
|
if stored.get("result_id").is_none() {
|
|
579
618
|
if let Some(obj) = stored.as_object_mut() {
|
|
580
|
-
obj.insert(
|
|
619
|
+
obj.insert(
|
|
620
|
+
"result_id".to_string(),
|
|
621
|
+
serde_json::Value::String(result_id.clone()),
|
|
622
|
+
);
|
|
581
623
|
}
|
|
582
624
|
}
|
|
583
625
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
584
|
-
let state_for_owner =
|
|
585
|
-
.unwrap_or(serde_json::json!({}));
|
|
626
|
+
let state_for_owner =
|
|
627
|
+
crate::state::persist::load_runtime_state(workspace).unwrap_or(serde_json::json!({}));
|
|
586
628
|
let owner_team = explicit_owner_team
|
|
587
629
|
.filter(|team| !team.is_empty())
|
|
588
630
|
.map(str::to_string)
|
|
@@ -612,12 +654,27 @@ pub fn report_result_for_owner_team(
|
|
|
612
654
|
"status".to_string(),
|
|
613
655
|
serde_json::Value::String("duplicate_ignored".to_string()),
|
|
614
656
|
);
|
|
615
|
-
out.insert(
|
|
616
|
-
|
|
617
|
-
|
|
657
|
+
out.insert(
|
|
658
|
+
"result_id".to_string(),
|
|
659
|
+
serde_json::Value::String(result_id),
|
|
660
|
+
);
|
|
661
|
+
out.insert(
|
|
662
|
+
"task_id".to_string(),
|
|
663
|
+
serde_json::Value::String(task_id.to_string()),
|
|
664
|
+
);
|
|
665
|
+
out.insert(
|
|
666
|
+
"agent_id".to_string(),
|
|
667
|
+
serde_json::Value::String(agent_id.to_string()),
|
|
668
|
+
);
|
|
618
669
|
out.insert("acknowledged_messages".to_string(), serde_json::json!([]));
|
|
619
|
-
out.insert(
|
|
620
|
-
|
|
670
|
+
out.insert(
|
|
671
|
+
"leader_notified".to_string(),
|
|
672
|
+
serde_json::Value::Bool(false),
|
|
673
|
+
);
|
|
674
|
+
out.insert(
|
|
675
|
+
"notification_message_id".to_string(),
|
|
676
|
+
serde_json::Value::Null,
|
|
677
|
+
);
|
|
621
678
|
out.insert(
|
|
622
679
|
"notification_status".to_string(),
|
|
623
680
|
serde_json::Value::String("duplicate_ignored".to_string()),
|
|
@@ -633,10 +690,11 @@ pub fn report_result_for_owner_team(
|
|
|
633
690
|
// primitive synchronously, NOT via a parallel queued scheduled_events row. The
|
|
634
691
|
// legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
|
|
635
692
|
// to the caller as "success" while leader actually never saw the result text).
|
|
636
|
-
let content =
|
|
693
|
+
let content =
|
|
694
|
+
format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
|
|
637
695
|
let state = report_owner_state(&state_for_owner, &owner_team);
|
|
638
696
|
let event_log = EventLog::new(workspace);
|
|
639
|
-
let mut outcome = super::leader_receiver::send_to_leader_receiver(
|
|
697
|
+
let mut outcome = match super::leader_receiver::send_to_leader_receiver(
|
|
640
698
|
workspace,
|
|
641
699
|
&state,
|
|
642
700
|
"leader",
|
|
@@ -646,37 +704,77 @@ pub fn report_result_for_owner_team(
|
|
|
646
704
|
false,
|
|
647
705
|
Some(&result_id),
|
|
648
706
|
&event_log,
|
|
649
|
-
)
|
|
707
|
+
) {
|
|
708
|
+
Ok(outcome) => outcome,
|
|
709
|
+
Err(error) => {
|
|
710
|
+
let message_id = format!("fallback:{result_id}");
|
|
711
|
+
let fallback_error =
|
|
712
|
+
fallback_primary_error_text(fallback_primary_error, format!("leader_funnel_error:{error}"));
|
|
713
|
+
super::leader_receiver::deliver_to_leader_fallback_pane(
|
|
714
|
+
workspace,
|
|
715
|
+
&state,
|
|
716
|
+
&message_id,
|
|
717
|
+
Some(&result_id),
|
|
718
|
+
&content,
|
|
719
|
+
false,
|
|
720
|
+
Some(&fallback_error),
|
|
721
|
+
&event_log,
|
|
722
|
+
)?
|
|
723
|
+
}
|
|
724
|
+
};
|
|
650
725
|
if let Some(message_id) = outcome.message_id.clone() {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
726
|
+
let store = MessageStore::open(workspace)?;
|
|
727
|
+
let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
728
|
+
let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
|
|
729
|
+
.unwrap_or_else(|_| state_for_owner.clone());
|
|
730
|
+
let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
|
|
731
|
+
let mut primary_error: Option<String> = None;
|
|
732
|
+
for attempt in 0..3 {
|
|
733
|
+
let _ = store.mark(&message_id, "accepted", None);
|
|
734
|
+
match super::delivery::deliver_pending_message(
|
|
735
|
+
workspace,
|
|
736
|
+
&store,
|
|
737
|
+
&transport,
|
|
738
|
+
&message_id,
|
|
739
|
+
&event_log,
|
|
740
|
+
&delivery_state,
|
|
741
|
+
) {
|
|
742
|
+
Ok(next) => outcome = next,
|
|
743
|
+
Err(error) => {
|
|
744
|
+
primary_error = Some(format!("primary_delivery_error:{error}"));
|
|
745
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
746
|
+
ok: false,
|
|
747
|
+
status: crate::messaging::DeliveryStatus::Failed,
|
|
748
|
+
message_status: super::helpers::MessageStatusShadow("failed".to_string()),
|
|
749
|
+
message_id: Some(message_id.clone()),
|
|
750
|
+
verification: Some(error.to_string()),
|
|
751
|
+
stage: None,
|
|
752
|
+
reason: None,
|
|
753
|
+
channel: Some("leader_receiver".to_string()),
|
|
754
|
+
};
|
|
667
755
|
break;
|
|
668
756
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
757
|
+
}
|
|
758
|
+
if outcome.ok {
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
match super::delivery::deliver_pending_messages(
|
|
762
|
+
workspace,
|
|
763
|
+
&delivery_state,
|
|
764
|
+
&transport,
|
|
765
|
+
&event_log,
|
|
766
|
+
) {
|
|
767
|
+
Ok(delivered)
|
|
768
|
+
if delivered
|
|
769
|
+
.iter()
|
|
770
|
+
.any(|delivered_id| delivered_id == &message_id) =>
|
|
771
|
+
{
|
|
676
772
|
outcome = crate::messaging::DeliveryOutcome {
|
|
677
773
|
ok: true,
|
|
678
774
|
status: crate::messaging::DeliveryStatus::Delivered,
|
|
679
|
-
message_status: super::helpers::MessageStatusShadow(
|
|
775
|
+
message_status: super::helpers::MessageStatusShadow(
|
|
776
|
+
"delivered".to_string(),
|
|
777
|
+
),
|
|
680
778
|
message_id: Some(message_id.clone()),
|
|
681
779
|
verification: None,
|
|
682
780
|
stage: None,
|
|
@@ -685,39 +783,46 @@ pub fn report_result_for_owner_team(
|
|
|
685
783
|
};
|
|
686
784
|
break;
|
|
687
785
|
}
|
|
688
|
-
|
|
689
|
-
|
|
786
|
+
Ok(_) => {}
|
|
787
|
+
Err(error) => {
|
|
788
|
+
primary_error = Some(format!("primary_delivery_error:{error}"));
|
|
789
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
790
|
+
ok: false,
|
|
791
|
+
status: crate::messaging::DeliveryStatus::Failed,
|
|
792
|
+
message_status: super::helpers::MessageStatusShadow("failed".to_string()),
|
|
793
|
+
message_id: Some(message_id.clone()),
|
|
794
|
+
verification: Some(error.to_string()),
|
|
795
|
+
stage: None,
|
|
796
|
+
reason: None,
|
|
797
|
+
channel: Some("leader_receiver".to_string()),
|
|
798
|
+
};
|
|
799
|
+
break;
|
|
690
800
|
}
|
|
691
801
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// 全失败 → direct inject 兜底投一条(不丢 result,守 #230/MUST-8)。两全:恰一条。
|
|
695
|
-
if !outcome.ok {
|
|
696
|
-
match inject_leader_notification_direct(workspace, &delivery_state, &content, &message_id) {
|
|
697
|
-
Ok(()) => {
|
|
698
|
-
store.mark(&message_id, "delivered", None)?;
|
|
699
|
-
outcome = crate::messaging::DeliveryOutcome {
|
|
700
|
-
ok: true,
|
|
701
|
-
status: crate::messaging::DeliveryStatus::Delivered,
|
|
702
|
-
message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
|
|
703
|
-
message_id: Some(message_id),
|
|
704
|
-
verification: None,
|
|
705
|
-
stage: None,
|
|
706
|
-
reason: None,
|
|
707
|
-
channel: Some("leader_receiver".to_string()),
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
Err(reason) => {
|
|
711
|
-
event_log.write(
|
|
712
|
-
"leader_receiver.direct_inject_skipped",
|
|
713
|
-
serde_json::json!({
|
|
714
|
-
"message_id": message_id,
|
|
715
|
-
"reason": reason,
|
|
716
|
-
}),
|
|
717
|
-
)?;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
802
|
+
if attempt < 2 {
|
|
803
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
720
804
|
}
|
|
805
|
+
}
|
|
806
|
+
if !outcome.ok {
|
|
807
|
+
let fallback_error = primary_error.unwrap_or_else(|| {
|
|
808
|
+
format!(
|
|
809
|
+
"leader_notification_primary_failed:{}",
|
|
810
|
+
super::helpers::status_wire(outcome.status)
|
|
811
|
+
)
|
|
812
|
+
});
|
|
813
|
+
let fallback_error =
|
|
814
|
+
fallback_primary_error_text(fallback_primary_error, fallback_error);
|
|
815
|
+
outcome = super::leader_receiver::deliver_to_leader_fallback_pane(
|
|
816
|
+
workspace,
|
|
817
|
+
&delivery_state,
|
|
818
|
+
&message_id,
|
|
819
|
+
Some(&result_id),
|
|
820
|
+
&content,
|
|
821
|
+
false,
|
|
822
|
+
Some(&fallback_error),
|
|
823
|
+
&event_log,
|
|
824
|
+
)?;
|
|
825
|
+
}
|
|
721
826
|
}
|
|
722
827
|
let leader_notified = outcome.ok;
|
|
723
828
|
let notification_status_wire = if outcome.ok {
|
|
@@ -729,7 +834,10 @@ pub fn report_result_for_owner_team(
|
|
|
729
834
|
} else {
|
|
730
835
|
"refused"
|
|
731
836
|
};
|
|
732
|
-
let channel = outcome
|
|
837
|
+
let channel = outcome
|
|
838
|
+
.channel
|
|
839
|
+
.clone()
|
|
840
|
+
.unwrap_or_else(|| "leader_receiver".to_string());
|
|
733
841
|
event_log.write(
|
|
734
842
|
"mcp.report_result",
|
|
735
843
|
serde_json::json!({
|
|
@@ -744,14 +852,28 @@ pub fn report_result_for_owner_team(
|
|
|
744
852
|
|
|
745
853
|
let mut out = serde_json::Map::new();
|
|
746
854
|
out.insert("ok".to_string(), serde_json::Value::Bool(true));
|
|
747
|
-
out.insert(
|
|
748
|
-
|
|
749
|
-
|
|
855
|
+
out.insert(
|
|
856
|
+
"result_id".to_string(),
|
|
857
|
+
serde_json::Value::String(result_id),
|
|
858
|
+
);
|
|
859
|
+
out.insert(
|
|
860
|
+
"task_id".to_string(),
|
|
861
|
+
serde_json::Value::String(task_id.to_string()),
|
|
862
|
+
);
|
|
863
|
+
out.insert(
|
|
864
|
+
"agent_id".to_string(),
|
|
865
|
+
serde_json::Value::String(agent_id.to_string()),
|
|
866
|
+
);
|
|
750
867
|
out.insert("acknowledged_messages".to_string(), serde_json::json!([]));
|
|
751
|
-
out.insert(
|
|
868
|
+
out.insert(
|
|
869
|
+
"leader_notified".to_string(),
|
|
870
|
+
serde_json::Value::Bool(leader_notified),
|
|
871
|
+
);
|
|
752
872
|
out.insert(
|
|
753
873
|
"notification_message_id".to_string(),
|
|
754
|
-
outcome
|
|
874
|
+
outcome
|
|
875
|
+
.message_id
|
|
876
|
+
.map_or(serde_json::Value::Null, serde_json::Value::String),
|
|
755
877
|
);
|
|
756
878
|
out.insert(
|
|
757
879
|
"notification_status".to_string(),
|
|
@@ -764,20 +886,28 @@ pub fn report_result_for_owner_team(
|
|
|
764
886
|
if channel == "rebind_required" {
|
|
765
887
|
out.insert(
|
|
766
888
|
"notification_action".to_string(),
|
|
767
|
-
serde_json::Value::String(
|
|
889
|
+
serde_json::Value::String(
|
|
890
|
+
"run team-agent claim-leader or team-agent takeover".to_string(),
|
|
891
|
+
),
|
|
768
892
|
);
|
|
769
893
|
}
|
|
770
894
|
out.insert("notification_event_id".to_string(), serde_json::Value::Null);
|
|
771
895
|
Ok(serde_json::Value::Object(out))
|
|
772
896
|
}
|
|
773
897
|
|
|
898
|
+
fn fallback_primary_error_text(cli_primary_error: Option<&str>, observed: String) -> String {
|
|
899
|
+
match cli_primary_error.filter(|error| !error.trim().is_empty()) {
|
|
900
|
+
Some(error) => format!("{error}; {observed}"),
|
|
901
|
+
None => observed,
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
774
905
|
fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
|
|
775
|
-
let mut state =
|
|
776
|
-
.canonical_key()
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
};
|
|
906
|
+
let mut state =
|
|
907
|
+
match crate::state::projection::resolve_owner_team_id(state, owner_team).canonical_key() {
|
|
908
|
+
Some(team) => crate::state::projection::project_top_level_view(state, team),
|
|
909
|
+
None => state.clone(),
|
|
910
|
+
};
|
|
781
911
|
if let Some(obj) = state.as_object_mut() {
|
|
782
912
|
obj.insert(
|
|
783
913
|
"active_team_key".to_string(),
|
|
@@ -787,56 +917,6 @@ fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json
|
|
|
787
917
|
state
|
|
788
918
|
}
|
|
789
919
|
|
|
790
|
-
fn inject_leader_notification_direct(
|
|
791
|
-
workspace: &Path,
|
|
792
|
-
state: &serde_json::Value,
|
|
793
|
-
content: &str,
|
|
794
|
-
message_id: &str,
|
|
795
|
-
) -> Result<(), String> {
|
|
796
|
-
let Some(pane_id) = state
|
|
797
|
-
.get("leader_receiver")
|
|
798
|
-
.or_else(|| state.get("team_owner"))
|
|
799
|
-
.and_then(|receiver| receiver.get("pane_id"))
|
|
800
|
-
.and_then(serde_json::Value::as_str)
|
|
801
|
-
.filter(|pane| !pane.is_empty() && *pane != "__team_agent_unbound__")
|
|
802
|
-
else {
|
|
803
|
-
return Err("leader_direct_inject_failed:no_bound_pane".to_string());
|
|
804
|
-
};
|
|
805
|
-
let rendered = format!(
|
|
806
|
-
"Team Agent message from leader_receiver:\n\n{content}\n\n[team-agent-token:{message_id}]"
|
|
807
|
-
);
|
|
808
|
-
let target = Target::Pane(PaneId::new(pane_id));
|
|
809
|
-
if let Some(socket) = state
|
|
810
|
-
.get("leader_receiver")
|
|
811
|
-
.and_then(|receiver| receiver.get("tmux_socket"))
|
|
812
|
-
.and_then(serde_json::Value::as_str)
|
|
813
|
-
.filter(|socket| !socket.is_empty())
|
|
814
|
-
{
|
|
815
|
-
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
816
|
-
if backend
|
|
817
|
-
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
818
|
-
.is_ok()
|
|
819
|
-
{
|
|
820
|
-
return Ok(());
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
824
|
-
if workspace_backend
|
|
825
|
-
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
826
|
-
.is_ok()
|
|
827
|
-
{
|
|
828
|
-
return Ok(());
|
|
829
|
-
}
|
|
830
|
-
let default_backend = crate::tmux_backend::TmuxBackend::new();
|
|
831
|
-
if default_backend
|
|
832
|
-
.inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
|
|
833
|
-
.is_ok()
|
|
834
|
-
{
|
|
835
|
-
return Ok(());
|
|
836
|
-
}
|
|
837
|
-
Err(format!("leader_direct_inject_failed:pane={pane_id}"))
|
|
838
|
-
}
|
|
839
|
-
|
|
840
920
|
fn insert_result_if_absent(
|
|
841
921
|
conn: &rusqlite::Connection,
|
|
842
922
|
result_id: &str,
|
|
@@ -888,7 +968,9 @@ fn format_report_result_notification(
|
|
|
888
968
|
}
|
|
889
969
|
|
|
890
970
|
fn format_report_result_tests(envelope: &serde_json::Value) -> Option<String> {
|
|
891
|
-
let tests = envelope
|
|
971
|
+
let tests = envelope
|
|
972
|
+
.get("tests")
|
|
973
|
+
.and_then(serde_json::Value::as_array)?;
|
|
892
974
|
let parts = tests
|
|
893
975
|
.iter()
|
|
894
976
|
.filter_map(|test| {
|
|
@@ -9,7 +9,7 @@ use crate::model::ids::{TaskId, TeamKey};
|
|
|
9
9
|
use crate::transport::{PaneId, Transport};
|
|
10
10
|
|
|
11
11
|
use super::helpers::{status_wire, MessageStatusShadow};
|
|
12
|
-
use super::leader_receiver::send_to_leader_receiver;
|
|
12
|
+
use super::leader_receiver::{send_to_leader_receiver, send_to_leader_receiver_with_message_id};
|
|
13
13
|
use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
|
|
14
14
|
|
|
15
15
|
/// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
|
|
@@ -89,7 +89,7 @@ pub fn send_message(
|
|
|
89
89
|
backfill_leader_binding_for_delivery_view(&mut state, &raw_state);
|
|
90
90
|
let recipient = match target {
|
|
91
91
|
MessageTarget::Single(target) if target == "leader" => {
|
|
92
|
-
let outcome =
|
|
92
|
+
let outcome = send_to_leader_receiver_with_message_id(
|
|
93
93
|
workspace,
|
|
94
94
|
&state,
|
|
95
95
|
"leader",
|
|
@@ -98,6 +98,7 @@ pub fn send_message(
|
|
|
98
98
|
&opts.sender,
|
|
99
99
|
opts.requires_ack,
|
|
100
100
|
None,
|
|
101
|
+
opts.message_id.as_deref(),
|
|
101
102
|
&event_log,
|
|
102
103
|
)?;
|
|
103
104
|
if matches!(outcome.status, DeliveryStatus::Queued) && owner_pane_is_dead(&state) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn e23_fallback_pane_has_single_shared_noisy_surface() {
|
|
3
|
+
let leader_receiver = include_str!("../leader_receiver.rs");
|
|
4
|
+
assert!(leader_receiver.contains("deliver_to_leader_fallback_pane"));
|
|
5
|
+
assert!(leader_receiver.contains("leader_receiver.fallback_pane_attempt"));
|
|
6
|
+
assert!(leader_receiver.contains("leader_receiver.fallback_pane_submitted"));
|
|
7
|
+
assert!(leader_receiver.contains("leader_receiver.fallback_pane_failed"));
|
|
8
|
+
assert!(leader_receiver.contains("delivered_via=fallback_pane"));
|
|
9
|
+
assert!(
|
|
10
|
+
leader_receiver.find("if primary_ok").unwrap()
|
|
11
|
+
< leader_receiver
|
|
12
|
+
.find("leader_receiver.fallback_pane_attempt")
|
|
13
|
+
.unwrap(),
|
|
14
|
+
"primary-ok guard must run before any fallback audit/inject attempt"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn e23_no_parallel_direct_inject_or_fake_worker_mirror() {
|
|
20
|
+
let results = include_str!("../results.rs");
|
|
21
|
+
assert!(
|
|
22
|
+
!results.contains("inject_leader_notification_direct"),
|
|
23
|
+
"report_result must use the shared fallback pane primitive, not a private direct-inject path"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let fake_worker = include_str!("../../fake_worker.rs");
|
|
27
|
+
assert!(
|
|
28
|
+
!fake_worker.contains("mirror_fake_result_to_leader"),
|
|
29
|
+
"fake_worker must not keep an independent leader pane mirror"
|
|
30
|
+
);
|
|
31
|
+
assert!(
|
|
32
|
+
!fake_worker.contains("Transport::inject"),
|
|
33
|
+
"fake_worker must report through report_result_for_owner_team, not inject leader panes directly"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -275,7 +275,10 @@ impl EnvGuardMsg {
|
|
|
275
275
|
Some(v) => std::env::set_var(key, v),
|
|
276
276
|
None => std::env::remove_var(key),
|
|
277
277
|
}
|
|
278
|
-
Self {
|
|
278
|
+
Self {
|
|
279
|
+
key: key.to_string(),
|
|
280
|
+
prev,
|
|
281
|
+
}
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
impl Drop for EnvGuardMsg {
|
|
@@ -288,6 +291,7 @@ impl Drop for EnvGuardMsg {
|
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
mod basic;
|
|
294
|
+
mod e23;
|
|
295
|
+
mod main_preserved;
|
|
291
296
|
mod runtime;
|
|
292
297
|
mod spine;
|
|
293
|
-
mod main_preserved;
|