@team-agent/installer 0.5.1 → 0.5.2
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 +118 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +74 -1
- package/crates/team-agent/src/cli/mod.rs +70 -25
- package/crates/team-agent/src/cli/named_address.rs +111 -0
- package/crates/team-agent/src/cli/send.rs +98 -3
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/named_address.rs +131 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/codex_app_server.rs +488 -0
- package/crates/team-agent/src/conpty/backend.rs +710 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +45 -6
- package/crates/team-agent/src/diagnose/orphans.rs +11 -3
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
- package/crates/team-agent/src/lib.rs +23 -0
- package/crates/team-agent/src/lifecycle/launch.rs +127 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
- package/crates/team-agent/src/tmux_backend.rs +31 -0
- package/crates/team-agent/src/transport_factory.rs +632 -0
- package/package.json +4 -4
|
@@ -179,6 +179,29 @@ pub fn deliver_pending_message(
|
|
|
179
179
|
channel: None,
|
|
180
180
|
});
|
|
181
181
|
};
|
|
182
|
+
if message.recipient == "leader" {
|
|
183
|
+
if let Some(receiver) = leader_receiver_value(state) {
|
|
184
|
+
if let Some(outcome) = leader_receiver_transport_conflict_outcome(
|
|
185
|
+
store,
|
|
186
|
+
event_log,
|
|
187
|
+
message_id,
|
|
188
|
+
receiver,
|
|
189
|
+
&message.sender,
|
|
190
|
+
)? {
|
|
191
|
+
return Ok(outcome);
|
|
192
|
+
}
|
|
193
|
+
if crate::codex_app_server::receiver_is_app_server(receiver) {
|
|
194
|
+
return deliver_leader_via_app_server(
|
|
195
|
+
store,
|
|
196
|
+
event_log,
|
|
197
|
+
state,
|
|
198
|
+
message_id,
|
|
199
|
+
&message,
|
|
200
|
+
canonical_owner_team_id.as_deref(),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
182
205
|
if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
|
|
183
206
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
184
207
|
event_log.write(
|
|
@@ -604,6 +627,223 @@ pub fn render_message(
|
|
|
604
627
|
format!("{header}:\n\n{content}\n\n[team-agent-token:{message_id}]")
|
|
605
628
|
}
|
|
606
629
|
|
|
630
|
+
fn deliver_leader_via_app_server(
|
|
631
|
+
store: &MessageStore,
|
|
632
|
+
event_log: &EventLog,
|
|
633
|
+
state: &serde_json::Value,
|
|
634
|
+
message_id: &str,
|
|
635
|
+
message: &PendingMessage,
|
|
636
|
+
owner_team_id: Option<&str>,
|
|
637
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
638
|
+
let receiver = leader_receiver_value(state).ok_or_else(|| {
|
|
639
|
+
MessagingError::Routing("codex_app_server leader_receiver missing".to_string())
|
|
640
|
+
})?;
|
|
641
|
+
let binding = match crate::codex_app_server::binding_from_receiver(receiver) {
|
|
642
|
+
Ok(binding) => binding,
|
|
643
|
+
Err(error) => {
|
|
644
|
+
return Ok(app_server_delivery_failure(
|
|
645
|
+
store,
|
|
646
|
+
event_log,
|
|
647
|
+
receiver,
|
|
648
|
+
message_id,
|
|
649
|
+
&message.sender,
|
|
650
|
+
owner_team_id,
|
|
651
|
+
&error,
|
|
652
|
+
)?);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
let rendered = render_message(
|
|
656
|
+
&message.sender,
|
|
657
|
+
message.task_id.as_deref(),
|
|
658
|
+
&message.content,
|
|
659
|
+
message_id,
|
|
660
|
+
);
|
|
661
|
+
match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, &rendered) {
|
|
662
|
+
Ok(submit) => {
|
|
663
|
+
store.mark(message_id, "delivered", None)?;
|
|
664
|
+
event_log.write(
|
|
665
|
+
"message.delivered",
|
|
666
|
+
serde_json::json!({"message_id": message_id}),
|
|
667
|
+
)?;
|
|
668
|
+
event_log.write(
|
|
669
|
+
"leader_receiver.app_server_submitted",
|
|
670
|
+
serde_json::json!({
|
|
671
|
+
"message_id": message_id,
|
|
672
|
+
"owner_team_id": owner_team_id,
|
|
673
|
+
"owner_epoch": receiver_owner_epoch(receiver),
|
|
674
|
+
"socket": binding.socket,
|
|
675
|
+
"thread_id": binding.thread_id,
|
|
676
|
+
"turn_id": submit.turn_id,
|
|
677
|
+
"turn_status": submit.turn_status,
|
|
678
|
+
}),
|
|
679
|
+
)?;
|
|
680
|
+
Ok(DeliveryOutcome {
|
|
681
|
+
ok: true,
|
|
682
|
+
status: DeliveryStatus::Delivered,
|
|
683
|
+
message_status: MessageStatusShadow("delivered".to_string()),
|
|
684
|
+
message_id: Some(message_id.to_string()),
|
|
685
|
+
verification: None,
|
|
686
|
+
stage: None,
|
|
687
|
+
reason: None,
|
|
688
|
+
channel: Some("codex_app_server".to_string()),
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
Err(error) => app_server_delivery_failure(
|
|
692
|
+
store,
|
|
693
|
+
event_log,
|
|
694
|
+
receiver,
|
|
695
|
+
message_id,
|
|
696
|
+
&message.sender,
|
|
697
|
+
owner_team_id,
|
|
698
|
+
&error,
|
|
699
|
+
),
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fn app_server_delivery_failure(
|
|
704
|
+
store: &MessageStore,
|
|
705
|
+
event_log: &EventLog,
|
|
706
|
+
receiver: &serde_json::Value,
|
|
707
|
+
message_id: &str,
|
|
708
|
+
sender: &str,
|
|
709
|
+
owner_team_id: Option<&str>,
|
|
710
|
+
error: &crate::codex_app_server::AppServerError,
|
|
711
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
712
|
+
let action = app_server_rebind_action(owner_team_id, receiver);
|
|
713
|
+
match error {
|
|
714
|
+
crate::codex_app_server::AppServerError::LeaderBusy(message) => {
|
|
715
|
+
store.mark(message_id, "target_resolved", Some("leader_busy"))?;
|
|
716
|
+
event_log.write(
|
|
717
|
+
"leader_receiver.app_server_busy",
|
|
718
|
+
serde_json::json!({
|
|
719
|
+
"message_id": message_id,
|
|
720
|
+
"sender": sender,
|
|
721
|
+
"reason": "leader_busy",
|
|
722
|
+
"error": message,
|
|
723
|
+
}),
|
|
724
|
+
)?;
|
|
725
|
+
Ok(DeliveryOutcome {
|
|
726
|
+
ok: false,
|
|
727
|
+
status: DeliveryStatus::RetryScheduled,
|
|
728
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
729
|
+
message_id: Some(message_id.to_string()),
|
|
730
|
+
verification: Some("leader_busy".to_string()),
|
|
731
|
+
stage: None,
|
|
732
|
+
reason: Some(DeliveryRefusal::RecipientBusy),
|
|
733
|
+
channel: Some("leader_busy".to_string()),
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
crate::codex_app_server::AppServerError::ThreadStale { expected, actual } => {
|
|
737
|
+
store.mark(message_id, "failed", Some(error.code()))?;
|
|
738
|
+
event_log.write(
|
|
739
|
+
"leader_receiver.app_server_thread_stale",
|
|
740
|
+
serde_json::json!({
|
|
741
|
+
"message_id": message_id,
|
|
742
|
+
"sender": sender,
|
|
743
|
+
"owner_team_id": owner_team_id,
|
|
744
|
+
"owner_epoch": receiver_owner_epoch(receiver),
|
|
745
|
+
"expected": expected,
|
|
746
|
+
"actual": actual,
|
|
747
|
+
"action": action,
|
|
748
|
+
}),
|
|
749
|
+
)?;
|
|
750
|
+
Ok(rebind_required_outcome(message_id, &action))
|
|
751
|
+
}
|
|
752
|
+
crate::codex_app_server::AppServerError::ApprovalUnsupported(method) => {
|
|
753
|
+
store.mark(message_id, "failed", Some("approval_unsupported"))?;
|
|
754
|
+
event_log.write(
|
|
755
|
+
"codex_app_server.approval_unsupported",
|
|
756
|
+
serde_json::json!({
|
|
757
|
+
"message_id": message_id,
|
|
758
|
+
"sender": sender,
|
|
759
|
+
"method": method,
|
|
760
|
+
"action": "handle approval in the Codex app-server session",
|
|
761
|
+
}),
|
|
762
|
+
)?;
|
|
763
|
+
Ok(DeliveryOutcome {
|
|
764
|
+
ok: false,
|
|
765
|
+
status: DeliveryStatus::Blocked,
|
|
766
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
767
|
+
message_id: Some(message_id.to_string()),
|
|
768
|
+
verification: Some("handle approval in the Codex app-server session".to_string()),
|
|
769
|
+
stage: None,
|
|
770
|
+
reason: Some(DeliveryRefusal::MissingPermissions),
|
|
771
|
+
channel: Some("codex_app_server".to_string()),
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
crate::codex_app_server::AppServerError::ProtocolMismatch(_)
|
|
775
|
+
| crate::codex_app_server::AppServerError::MissingUserAgent => {
|
|
776
|
+
store.mark(message_id, "failed", Some(error.code()))?;
|
|
777
|
+
event_log.write(
|
|
778
|
+
"leader_receiver.app_server_protocol_mismatch",
|
|
779
|
+
serde_json::json!({
|
|
780
|
+
"message_id": message_id,
|
|
781
|
+
"sender": sender,
|
|
782
|
+
"owner_team_id": owner_team_id,
|
|
783
|
+
"owner_epoch": receiver_owner_epoch(receiver),
|
|
784
|
+
"reason": error.code(),
|
|
785
|
+
"error": error.to_string(),
|
|
786
|
+
"action": action,
|
|
787
|
+
}),
|
|
788
|
+
)?;
|
|
789
|
+
Ok(rebind_required_outcome(message_id, &action))
|
|
790
|
+
}
|
|
791
|
+
crate::codex_app_server::AppServerError::SocketUnreachable(_)
|
|
792
|
+
| crate::codex_app_server::AppServerError::SocketOwnershipInvalid(_)
|
|
793
|
+
| crate::codex_app_server::AppServerError::ThreadNotLive(_)
|
|
794
|
+
| crate::codex_app_server::AppServerError::Io(_)
|
|
795
|
+
| crate::codex_app_server::AppServerError::Json(_) => {
|
|
796
|
+
store.mark(message_id, "failed", Some(error.code()))?;
|
|
797
|
+
event_log.write(
|
|
798
|
+
"leader_receiver.delivery_blocked",
|
|
799
|
+
serde_json::json!({
|
|
800
|
+
"message_id": message_id,
|
|
801
|
+
"sender": sender,
|
|
802
|
+
"reason": error.code(),
|
|
803
|
+
"channel": "rebind_required",
|
|
804
|
+
"action": action,
|
|
805
|
+
"error": error.to_string(),
|
|
806
|
+
}),
|
|
807
|
+
)?;
|
|
808
|
+
Ok(rebind_required_outcome(message_id, &action))
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
fn rebind_required_outcome(message_id: &str, action: &str) -> DeliveryOutcome {
|
|
814
|
+
DeliveryOutcome {
|
|
815
|
+
ok: false,
|
|
816
|
+
status: DeliveryStatus::Refused,
|
|
817
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
818
|
+
message_id: Some(message_id.to_string()),
|
|
819
|
+
verification: Some(action.to_string()),
|
|
820
|
+
stage: None,
|
|
821
|
+
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
822
|
+
channel: Some("rebind_required".to_string()),
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
fn app_server_rebind_action(owner_team_id: Option<&str>, receiver: &serde_json::Value) -> String {
|
|
827
|
+
let team = owner_team_id
|
|
828
|
+
.filter(|team| !team.is_empty())
|
|
829
|
+
.map(|team| format!(" --team {team}"))
|
|
830
|
+
.unwrap_or_default();
|
|
831
|
+
let Some(app) = receiver.get("app_server") else {
|
|
832
|
+
return format!("run team-agent attach-app-server-leader{team} --socket <socket> --thread-id <thread_id>");
|
|
833
|
+
};
|
|
834
|
+
let socket = app
|
|
835
|
+
.get("socket")
|
|
836
|
+
.and_then(serde_json::Value::as_str)
|
|
837
|
+
.unwrap_or("<socket>");
|
|
838
|
+
let thread_id = app
|
|
839
|
+
.get("thread_id")
|
|
840
|
+
.and_then(serde_json::Value::as_str)
|
|
841
|
+
.unwrap_or("<thread_id>");
|
|
842
|
+
format!(
|
|
843
|
+
"run team-agent attach-app-server-leader{team} --socket {socket} --thread-id {thread_id}"
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
607
847
|
/// Resolve a recipient agent-id to a tmux-RESOLVABLE inject target: the persisted pane-id if present,
|
|
608
848
|
/// else a session-qualified `SessionWindow` (state.session_name + the agent's window, defaulting to the
|
|
609
849
|
/// id). NEVER the bare agent-id as a pane — a clientless coordinator cannot resolve that
|
|
@@ -770,6 +1010,70 @@ fn leader_receiver_pane_id(state: &serde_json::Value) -> Option<&str> {
|
|
|
770
1010
|
.or_else(|| only_team_entry(state).and_then(leader_receiver_pane_id_in_state))
|
|
771
1011
|
}
|
|
772
1012
|
|
|
1013
|
+
fn leader_receiver_value(state: &serde_json::Value) -> Option<&serde_json::Value> {
|
|
1014
|
+
leader_receiver_value_in_state(state)
|
|
1015
|
+
.or_else(|| active_team_entry(state).and_then(leader_receiver_value_in_state))
|
|
1016
|
+
.or_else(|| only_team_entry(state).and_then(leader_receiver_value_in_state))
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
fn leader_receiver_value_in_state(state: &serde_json::Value) -> Option<&serde_json::Value> {
|
|
1020
|
+
state.get("leader_receiver")
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
fn receiver_owner_epoch(receiver: &serde_json::Value) -> Option<u64> {
|
|
1024
|
+
receiver
|
|
1025
|
+
.get("owner_epoch")
|
|
1026
|
+
.and_then(serde_json::Value::as_u64)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
fn leader_receiver_transport_conflict_outcome(
|
|
1030
|
+
store: &MessageStore,
|
|
1031
|
+
event_log: &EventLog,
|
|
1032
|
+
message_id: &str,
|
|
1033
|
+
receiver: &serde_json::Value,
|
|
1034
|
+
sender: &str,
|
|
1035
|
+
) -> Result<Option<DeliveryOutcome>, MessagingError> {
|
|
1036
|
+
let mode = receiver.get("mode").and_then(serde_json::Value::as_str);
|
|
1037
|
+
let transport_kind = receiver
|
|
1038
|
+
.get("transport_kind")
|
|
1039
|
+
.and_then(serde_json::Value::as_str);
|
|
1040
|
+
if let (Some(mode), Some(transport_kind)) = (mode, transport_kind) {
|
|
1041
|
+
if !mode.is_empty() && !transport_kind.is_empty() && mode != transport_kind {
|
|
1042
|
+
store.mark(
|
|
1043
|
+
message_id,
|
|
1044
|
+
"failed",
|
|
1045
|
+
Some("leader_receiver_transport_conflict"),
|
|
1046
|
+
)?;
|
|
1047
|
+
event_log.write(
|
|
1048
|
+
"leader_receiver.delivery_blocked",
|
|
1049
|
+
serde_json::json!({
|
|
1050
|
+
"message_id": message_id,
|
|
1051
|
+
"sender": sender,
|
|
1052
|
+
"reason": "leader_receiver_transport_conflict",
|
|
1053
|
+
"mode": mode,
|
|
1054
|
+
"transport_kind": transport_kind,
|
|
1055
|
+
"channel": "rebind_required",
|
|
1056
|
+
"action": "run team-agent claim-leader, takeover, or attach-app-server-leader",
|
|
1057
|
+
}),
|
|
1058
|
+
)?;
|
|
1059
|
+
return Ok(Some(DeliveryOutcome {
|
|
1060
|
+
ok: false,
|
|
1061
|
+
status: DeliveryStatus::Refused,
|
|
1062
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
1063
|
+
message_id: Some(message_id.to_string()),
|
|
1064
|
+
verification: Some(
|
|
1065
|
+
"run team-agent claim-leader, takeover, or attach-app-server-leader"
|
|
1066
|
+
.to_string(),
|
|
1067
|
+
),
|
|
1068
|
+
stage: None,
|
|
1069
|
+
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
1070
|
+
channel: Some("rebind_required".to_string()),
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
Ok(None)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
773
1077
|
fn leader_receiver_pane_binding(state: &serde_json::Value) -> Option<PaneSocketBinding<'_>> {
|
|
774
1078
|
leader_receiver_pane_binding_in_state(state)
|
|
775
1079
|
.or_else(|| active_team_entry(state).and_then(leader_receiver_pane_binding_in_state))
|
|
@@ -802,16 +1106,24 @@ fn leader_receiver_pane_is_usable(
|
|
|
802
1106
|
)
|
|
803
1107
|
}
|
|
804
1108
|
|
|
1109
|
+
/// 0.5.x Phase 1d Batch 4: the `Owned` variant now carries a boxed
|
|
1110
|
+
/// `Transport` trait object rather than a concrete `TmuxBackend`. All
|
|
1111
|
+
/// existing leader-fallback sites still supply a `TmuxBackend` here
|
|
1112
|
+
/// (Batch 4 is a helper replacement at the leader channel only —
|
|
1113
|
+
/// `transport_kind`-based dispatch belongs to the parallel
|
|
1114
|
+
/// `feat/appserver-leader-host` work per leader msg_4ac2122489e0),
|
|
1115
|
+
/// but the type is now open so a future conpty leader-hosting flow
|
|
1116
|
+
/// can plug in without another enum widen.
|
|
805
1117
|
enum DeliveryTransport<'a> {
|
|
806
1118
|
Borrowed(&'a dyn Transport),
|
|
807
|
-
Owned(
|
|
1119
|
+
Owned(Box<dyn Transport>),
|
|
808
1120
|
}
|
|
809
1121
|
|
|
810
1122
|
impl<'a> DeliveryTransport<'a> {
|
|
811
1123
|
fn as_transport(&'a self) -> &'a dyn Transport {
|
|
812
1124
|
match self {
|
|
813
1125
|
Self::Borrowed(transport) => *transport,
|
|
814
|
-
Self::Owned(transport) => transport,
|
|
1126
|
+
Self::Owned(transport) => transport.as_ref(),
|
|
815
1127
|
}
|
|
816
1128
|
}
|
|
817
1129
|
}
|
|
@@ -825,6 +1137,14 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
825
1137
|
if recipient != "leader" {
|
|
826
1138
|
return DeliveryTransport::Borrowed(product_transport);
|
|
827
1139
|
}
|
|
1140
|
+
// 0.5.x Phase 1d Batch 4: leader-fallback chain uses the factory
|
|
1141
|
+
// tmux channel helpers instead of `TmuxBackend::{new,for_workspace,
|
|
1142
|
+
// for_tmux_endpoint}` directly. Semantics are identical (helpers
|
|
1143
|
+
// are thin wrappers) but `grep -rn transport_factory::tmux_` at any
|
|
1144
|
+
// future point returns every intentional tmux-only leader-channel
|
|
1145
|
+
// site. Leader-channel dispatch by `transport_kind` is the parallel
|
|
1146
|
+
// `feat/appserver-leader-host` work — this batch stays helper-swap
|
|
1147
|
+
// only per leader msg_4ac2122489e0.
|
|
828
1148
|
let pane_id = leader_receiver_pane_id(state);
|
|
829
1149
|
let Some(socket) = leader_receiver_tmux_socket(state) else {
|
|
830
1150
|
if let Some(pane_id) = pane_id {
|
|
@@ -834,14 +1154,14 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
834
1154
|
.iter()
|
|
835
1155
|
.any(|target| target.pane_id.as_str() == pane_id);
|
|
836
1156
|
if !in_workspace {
|
|
837
|
-
let default_backend = crate::
|
|
1157
|
+
let default_backend = crate::transport_factory::tmux_default_transport();
|
|
838
1158
|
if default_backend
|
|
839
1159
|
.list_targets()
|
|
840
1160
|
.unwrap_or_default()
|
|
841
1161
|
.iter()
|
|
842
1162
|
.any(|target| target.pane_id.as_str() == pane_id)
|
|
843
1163
|
{
|
|
844
|
-
return DeliveryTransport::Owned(default_backend);
|
|
1164
|
+
return DeliveryTransport::Owned(Box::new(default_backend));
|
|
845
1165
|
}
|
|
846
1166
|
}
|
|
847
1167
|
}
|
|
@@ -850,7 +1170,7 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
850
1170
|
if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
|
|
851
1171
|
DeliveryTransport::Borrowed(product_transport)
|
|
852
1172
|
} else {
|
|
853
|
-
let endpoint_backend = crate::
|
|
1173
|
+
let endpoint_backend = crate::transport_factory::tmux_endpoint_transport(socket);
|
|
854
1174
|
if let Some(pane_id) = pane_id {
|
|
855
1175
|
if endpoint_backend
|
|
856
1176
|
.list_targets()
|
|
@@ -858,7 +1178,7 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
858
1178
|
.iter()
|
|
859
1179
|
.any(|target| target.pane_id.as_str() == pane_id)
|
|
860
1180
|
{
|
|
861
|
-
return DeliveryTransport::Owned(endpoint_backend);
|
|
1181
|
+
return DeliveryTransport::Owned(Box::new(endpoint_backend));
|
|
862
1182
|
}
|
|
863
1183
|
if product_transport
|
|
864
1184
|
.list_targets()
|
|
@@ -868,17 +1188,17 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
868
1188
|
{
|
|
869
1189
|
return DeliveryTransport::Borrowed(product_transport);
|
|
870
1190
|
}
|
|
871
|
-
let default_backend = crate::
|
|
1191
|
+
let default_backend = crate::transport_factory::tmux_default_transport();
|
|
872
1192
|
if default_backend
|
|
873
1193
|
.list_targets()
|
|
874
1194
|
.unwrap_or_default()
|
|
875
1195
|
.iter()
|
|
876
1196
|
.any(|target| target.pane_id.as_str() == pane_id)
|
|
877
1197
|
{
|
|
878
|
-
return DeliveryTransport::Owned(default_backend);
|
|
1198
|
+
return DeliveryTransport::Owned(Box::new(default_backend));
|
|
879
1199
|
}
|
|
880
1200
|
}
|
|
881
|
-
DeliveryTransport::Owned(endpoint_backend)
|
|
1201
|
+
DeliveryTransport::Owned(Box::new(endpoint_backend))
|
|
882
1202
|
}
|
|
883
1203
|
}
|
|
884
1204
|
|
|
@@ -7,7 +7,7 @@ use serde_json::Value;
|
|
|
7
7
|
|
|
8
8
|
use crate::event_log::EventLog;
|
|
9
9
|
use crate::message_store::{MessageStore, NotificationClaimParams};
|
|
10
|
-
use crate::model::ids::
|
|
10
|
+
use crate::model::ids::TaskId;
|
|
11
11
|
use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
|
|
12
12
|
|
|
13
13
|
use super::helpers::MessageStatusShadow;
|
|
@@ -182,131 +182,6 @@ pub fn send_to_leader_receiver_with_message_id(
|
|
|
182
182
|
})
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/// `claim_leader_receiver` (`leader.py:348`):认领/接管 leader pane + `owner_epoch++`。身份判定
|
|
186
|
-
/// 借 step 10 原语 (`_target_matches_owner_identity`/`_leader_command_looks_usable`)。
|
|
187
|
-
pub fn claim_leader_receiver(
|
|
188
|
-
workspace: &Path,
|
|
189
|
-
state: &mut serde_json::Value,
|
|
190
|
-
candidate: &serde_json::Value,
|
|
191
|
-
event_log: &EventLog,
|
|
192
|
-
confirm: bool,
|
|
193
|
-
expected_epoch: Option<OwnerEpoch>,
|
|
194
|
-
) -> Result<serde_json::Value, MessagingError> {
|
|
195
|
-
if !confirm {
|
|
196
|
-
return Ok(serde_json::json!({
|
|
197
|
-
"ok": false,
|
|
198
|
-
"status": "refused",
|
|
199
|
-
"reason": "confirm_required",
|
|
200
|
-
"action": "team-agent claim-leader --confirm",
|
|
201
|
-
}));
|
|
202
|
-
}
|
|
203
|
-
if let Some(expected) = expected_epoch {
|
|
204
|
-
if owner_epoch(state).is_some_and(|current| current != expected.0) {
|
|
205
|
-
let owner_epoch = owner_epoch(state).unwrap_or(0);
|
|
206
|
-
let bound_pane_id = leader_pane_id(state).map(ToString::to_string);
|
|
207
|
-
let value = serde_json::json!({
|
|
208
|
-
"ok": false,
|
|
209
|
-
"status": "refused",
|
|
210
|
-
"reason": "owner_epoch_advanced",
|
|
211
|
-
"owner_epoch": owner_epoch,
|
|
212
|
-
"bound_pane_id": bound_pane_id,
|
|
213
|
-
});
|
|
214
|
-
event_log.write("leader_receiver.claim_refused", value.clone())?;
|
|
215
|
-
return Ok(value);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
let candidate_pane = candidate.get("pane_id").and_then(Value::as_str);
|
|
219
|
-
if candidate_pane.is_some() && candidate_pane == leader_pane_id(state) {
|
|
220
|
-
return Ok(serde_json::json!({
|
|
221
|
-
"ok": true,
|
|
222
|
-
"status": "already_bound",
|
|
223
|
-
"pane_id": candidate_pane,
|
|
224
|
-
"owner_epoch": owner_epoch(state).unwrap_or(0),
|
|
225
|
-
}));
|
|
226
|
-
}
|
|
227
|
-
let next_epoch = owner_epoch(state).unwrap_or(0).saturating_add(1);
|
|
228
|
-
let Some(root) = state.as_object_mut() else {
|
|
229
|
-
return Err(MessagingError::Routing(
|
|
230
|
-
"runtime state root is not an object".to_string(),
|
|
231
|
-
));
|
|
232
|
-
};
|
|
233
|
-
// Build the updated owner + receiver values in-place (field-level
|
|
234
|
-
// mutation preserves any extra fields the caller carries), then publish
|
|
235
|
-
// through the ownership repository. This keeps `claim_leader_receiver`'s
|
|
236
|
-
// legacy field-merge semantics while sourcing all owner mutations from
|
|
237
|
-
// one API surface (Stage 3a, architect direction 2026-06-23).
|
|
238
|
-
let mut owner_value = root
|
|
239
|
-
.get("team_owner")
|
|
240
|
-
.cloned()
|
|
241
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
242
|
-
if let Some(owner) = owner_value.as_object_mut() {
|
|
243
|
-
owner.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
|
|
244
|
-
}
|
|
245
|
-
let mut receiver_value = root
|
|
246
|
-
.get("leader_receiver")
|
|
247
|
-
.cloned()
|
|
248
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
249
|
-
if let Some(receiver) = receiver_value.as_object_mut() {
|
|
250
|
-
receiver.insert("mode".to_string(), serde_json::json!("direct_tmux"));
|
|
251
|
-
receiver.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
|
|
252
|
-
copy_candidate_field(receiver, candidate, "pane_id");
|
|
253
|
-
copy_candidate_field(receiver, candidate, "provider");
|
|
254
|
-
copy_candidate_field(receiver, candidate, "leader_session_uuid");
|
|
255
|
-
if let Some(socket) = candidate
|
|
256
|
-
.get("tmux_socket")
|
|
257
|
-
.and_then(Value::as_str)
|
|
258
|
-
.filter(|socket| std::path::Path::new(socket).is_absolute())
|
|
259
|
-
.map(str::to_string)
|
|
260
|
-
.or_else(crate::tmux_backend::socket_name_from_tmux_env)
|
|
261
|
-
{
|
|
262
|
-
receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// The `root` borrow above is released by NLL before this call.
|
|
266
|
-
let team_key = crate::state::projection::team_state_key(state);
|
|
267
|
-
let record = crate::state::ownership::OwnershipWrite::new()
|
|
268
|
-
.with_team_owner(owner_value)
|
|
269
|
-
.with_leader_receiver(receiver_value)
|
|
270
|
-
.with_owner_epoch(next_epoch);
|
|
271
|
-
crate::state::ownership::write_owner(state, &team_key, record);
|
|
272
|
-
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
273
|
-
workspace,
|
|
274
|
-
state,
|
|
275
|
-
|latest| {
|
|
276
|
-
let team_key = crate::state::projection::team_state_key(latest);
|
|
277
|
-
let record = crate::state::ownership::OwnershipWrite::new()
|
|
278
|
-
.with_team_owner(
|
|
279
|
-
state
|
|
280
|
-
.get("team_owner")
|
|
281
|
-
.cloned()
|
|
282
|
-
.unwrap_or_else(|| serde_json::json!({})),
|
|
283
|
-
)
|
|
284
|
-
.with_leader_receiver(
|
|
285
|
-
state
|
|
286
|
-
.get("leader_receiver")
|
|
287
|
-
.cloned()
|
|
288
|
-
.unwrap_or_else(|| serde_json::json!({})),
|
|
289
|
-
)
|
|
290
|
-
.with_owner_epoch(next_epoch);
|
|
291
|
-
crate::state::ownership::write_owner(latest, &team_key, record);
|
|
292
|
-
},
|
|
293
|
-
)?;
|
|
294
|
-
event_log.write(
|
|
295
|
-
"leader_receiver.claimed",
|
|
296
|
-
serde_json::json!({"owner_epoch": next_epoch, "candidate": candidate}),
|
|
297
|
-
)?;
|
|
298
|
-
let receiver = state
|
|
299
|
-
.get("leader_receiver")
|
|
300
|
-
.cloned()
|
|
301
|
-
.unwrap_or_else(|| candidate.clone());
|
|
302
|
-
Ok(serde_json::json!({
|
|
303
|
-
"ok": true,
|
|
304
|
-
"status": "claimed",
|
|
305
|
-
"leader_receiver": receiver,
|
|
306
|
-
"owner_epoch": next_epoch,
|
|
307
|
-
}))
|
|
308
|
-
}
|
|
309
|
-
|
|
310
185
|
/// `_mirror_peer_message_to_leader` (`leader.py:31`):peer→peer 消息镜像到 leader receiver。
|
|
311
186
|
pub fn mirror_peer_message_to_leader(
|
|
312
187
|
workspace: &Path,
|
|
@@ -411,18 +286,25 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
411
286
|
let rendered = render_fallback_pane_message(content, message_id, primary_error);
|
|
412
287
|
let target = Target::Pane(PaneId::new(&pane_id));
|
|
413
288
|
let payload = InjectPayload::Text(rendered);
|
|
289
|
+
// 0.5.x Phase 1d Batch 4: leader-fallback pane inject uses the
|
|
290
|
+
// factory tmux channel helpers so `grep transport_factory::tmux_`
|
|
291
|
+
// enumerates every tmux-only leader-channel site. Semantics are
|
|
292
|
+
// identical to the previous direct `TmuxBackend::*` calls
|
|
293
|
+
// (helpers are thin wrappers). `transport_kind`-based dispatch
|
|
294
|
+
// is the parallel `feat/appserver-leader-host` work; this batch
|
|
295
|
+
// stays helper-swap only.
|
|
414
296
|
let inject_result = leader_tmux_socket(state)
|
|
415
297
|
.and_then(|socket| {
|
|
416
|
-
let backend = crate::
|
|
298
|
+
let backend = crate::transport_factory::tmux_endpoint_transport(socket);
|
|
417
299
|
backend.inject(&target, &payload, Key::Enter, true).ok()
|
|
418
300
|
})
|
|
419
301
|
.map(Ok)
|
|
420
302
|
.unwrap_or_else(|| {
|
|
421
|
-
let backend = crate::
|
|
303
|
+
let backend = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
422
304
|
backend.inject(&target, &payload, Key::Enter, true)
|
|
423
305
|
})
|
|
424
306
|
.or_else(|_| {
|
|
425
|
-
let backend = crate::
|
|
307
|
+
let backend = crate::transport_factory::tmux_default_transport();
|
|
426
308
|
backend.inject(&target, &payload, Key::Enter, true)
|
|
427
309
|
});
|
|
428
310
|
|
|
@@ -563,18 +445,21 @@ pub(crate) fn leader_pane_bound_but_not_live(workspace: &Path, state: &Value) ->
|
|
|
563
445
|
}
|
|
564
446
|
|
|
565
447
|
fn leader_pane_is_live(workspace: &Path, state: &Value, pane_id: &str) -> bool {
|
|
448
|
+
// 0.5.x Phase 1d Batch 4: leader liveness uses factory tmux
|
|
449
|
+
// channel helpers. Semantics unchanged; `transport_kind`-based
|
|
450
|
+
// dispatch is the parallel feat/appserver-leader-host work.
|
|
566
451
|
if let Some(socket) = leader_tmux_socket(state) {
|
|
567
|
-
return crate::
|
|
452
|
+
return crate::transport_factory::tmux_endpoint_transport(socket)
|
|
568
453
|
.list_targets()
|
|
569
454
|
.unwrap_or_default()
|
|
570
455
|
.iter()
|
|
571
456
|
.any(|target| target.pane_id.as_str() == pane_id);
|
|
572
457
|
}
|
|
573
|
-
let mut targets = crate::
|
|
458
|
+
let mut targets = crate::transport_factory::tmux_workspace_transport(workspace)
|
|
574
459
|
.list_targets()
|
|
575
460
|
.unwrap_or_default();
|
|
576
461
|
targets.extend(
|
|
577
|
-
crate::
|
|
462
|
+
crate::transport_factory::tmux_default_transport()
|
|
578
463
|
.list_targets()
|
|
579
464
|
.unwrap_or_default(),
|
|
580
465
|
);
|
|
@@ -621,9 +506,3 @@ fn render_fallback_pane_message(content: &str, message_id: &str, primary_error:
|
|
|
621
506
|
[team-agent-token:{message_id}]"
|
|
622
507
|
)
|
|
623
508
|
}
|
|
624
|
-
|
|
625
|
-
fn copy_candidate_field(out: &mut serde_json::Map<String, Value>, candidate: &Value, key: &str) {
|
|
626
|
-
if let Some(value) = candidate.get(key) {
|
|
627
|
-
out.insert(key.to_string(), value.clone());
|
|
628
|
-
}
|
|
629
|
-
}
|
|
@@ -83,8 +83,8 @@ pub use delivery::{
|
|
|
83
83
|
};
|
|
84
84
|
pub use helpers::fail_leader_delivery;
|
|
85
85
|
pub use leader_receiver::{
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
deliver_to_leader_fallback_pane, mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
87
|
+
send_to_leader_receiver_with_message_id,
|
|
88
88
|
};
|
|
89
89
|
pub use peers::allow_peer_talk;
|
|
90
90
|
pub use results::{
|