@team-agent/installer 0.5.0 → 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.
Files changed (40) hide show
  1. package/Cargo.lock +118 -9
  2. package/Cargo.toml +2 -2
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/app_server_test_support.rs +258 -0
  5. package/crates/team-agent/src/cli/adapters.rs +32 -3
  6. package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
  7. package/crates/team-agent/src/cli/diagnose.rs +14 -5
  8. package/crates/team-agent/src/cli/emit.rs +111 -3
  9. package/crates/team-agent/src/cli/mod.rs +72 -25
  10. package/crates/team-agent/src/cli/named_address.rs +975 -0
  11. package/crates/team-agent/src/cli/send.rs +200 -1
  12. package/crates/team-agent/src/cli/status_port.rs +44 -6
  13. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  14. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  15. package/crates/team-agent/src/cli/tests/named_address.rs +586 -0
  16. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  17. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  18. package/crates/team-agent/src/cli/types.rs +21 -0
  19. package/crates/team-agent/src/codex_app_server.rs +488 -0
  20. package/crates/team-agent/src/conpty/backend.rs +710 -0
  21. package/crates/team-agent/src/conpty/mod.rs +32 -0
  22. package/crates/team-agent/src/coordinator/backoff.rs +45 -6
  23. package/crates/team-agent/src/diagnose/orphans.rs +11 -3
  24. package/crates/team-agent/src/leader/lease.rs +144 -7
  25. package/crates/team-agent/src/leader/owner_bind.rs +5 -2
  26. package/crates/team-agent/src/leader/start.rs +18 -5
  27. package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
  28. package/crates/team-agent/src/lib.rs +23 -0
  29. package/crates/team-agent/src/lifecycle/launch.rs +127 -3
  30. package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
  31. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
  32. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
  33. package/crates/team-agent/src/messaging/delivery.rs +329 -9
  34. package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
  35. package/crates/team-agent/src/messaging/mod.rs +2 -2
  36. package/crates/team-agent/src/messaging/results.rs +78 -21
  37. package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
  38. package/crates/team-agent/src/tmux_backend.rs +31 -0
  39. package/crates/team-agent/src/transport_factory.rs +632 -0
  40. 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(crate::tmux_backend::TmuxBackend),
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::tmux_backend::TmuxBackend::new();
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::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
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::tmux_backend::TmuxBackend::new();
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::{OwnerEpoch, TaskId};
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::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
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::tmux_backend::TmuxBackend::for_workspace(workspace);
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::tmux_backend::TmuxBackend::new();
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::tmux_backend::TmuxBackend::for_tmux_endpoint(socket)
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::tmux_backend::TmuxBackend::for_workspace(workspace)
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::tmux_backend::TmuxBackend::new()
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
- claim_leader_receiver, deliver_to_leader_fallback_pane, mirror_peer_message_to_leader,
87
- send_to_leader_receiver, send_to_leader_receiver_with_message_id,
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::{