@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.
Files changed (37) 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 +74 -1
  9. package/crates/team-agent/src/cli/mod.rs +70 -25
  10. package/crates/team-agent/src/cli/named_address.rs +111 -0
  11. package/crates/team-agent/src/cli/send.rs +98 -3
  12. package/crates/team-agent/src/cli/status_port.rs +44 -6
  13. package/crates/team-agent/src/cli/tests/named_address.rs +131 -0
  14. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  15. package/crates/team-agent/src/cli/types.rs +17 -0
  16. package/crates/team-agent/src/codex_app_server.rs +488 -0
  17. package/crates/team-agent/src/conpty/backend.rs +710 -0
  18. package/crates/team-agent/src/conpty/mod.rs +32 -0
  19. package/crates/team-agent/src/coordinator/backoff.rs +45 -6
  20. package/crates/team-agent/src/diagnose/orphans.rs +11 -3
  21. package/crates/team-agent/src/leader/lease.rs +144 -7
  22. package/crates/team-agent/src/leader/owner_bind.rs +5 -2
  23. package/crates/team-agent/src/leader/start.rs +18 -5
  24. package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
  25. package/crates/team-agent/src/lib.rs +23 -0
  26. package/crates/team-agent/src/lifecycle/launch.rs +127 -3
  27. package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
  28. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
  29. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
  30. package/crates/team-agent/src/messaging/delivery.rs +329 -9
  31. package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
  32. package/crates/team-agent/src/messaging/mod.rs +2 -2
  33. package/crates/team-agent/src/messaging/results.rs +78 -21
  34. package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
  35. package/crates/team-agent/src/tmux_backend.rs +31 -0
  36. package/crates/team-agent/src/transport_factory.rs +632 -0
  37. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -91,7 +91,15 @@ dependencies = [
91
91
  "js-sys",
92
92
  "num-traits",
93
93
  "wasm-bindgen",
94
- "windows-link",
94
+ "windows-link 0.2.1",
95
+ ]
96
+
97
+ [[package]]
98
+ name = "conpty-transport"
99
+ version = "0.1.0"
100
+ dependencies = [
101
+ "serde",
102
+ "serde_json",
95
103
  ]
96
104
 
97
105
  [[package]]
@@ -244,7 +252,7 @@ dependencies = [
244
252
  "js-sys",
245
253
  "log",
246
254
  "wasm-bindgen",
247
- "windows-core",
255
+ "windows-core 0.62.2",
248
256
  ]
249
257
 
250
258
  [[package]]
@@ -357,7 +365,7 @@ dependencies = [
357
365
  "libc",
358
366
  "redox_syscall",
359
367
  "smallvec",
360
- "windows-link",
368
+ "windows-link 0.2.1",
361
369
  ]
362
370
 
363
371
  [[package]]
@@ -566,10 +574,11 @@ dependencies = [
566
574
 
567
575
  [[package]]
568
576
  name = "team-agent"
569
- version = "0.5.1"
577
+ version = "0.5.2"
570
578
  dependencies = [
571
579
  "anyhow",
572
580
  "chrono",
581
+ "conpty-transport",
573
582
  "libc",
574
583
  "regex",
575
584
  "rusqlite",
@@ -669,6 +678,17 @@ dependencies = [
669
678
  "unicode-ident",
670
679
  ]
671
680
 
681
+ [[package]]
682
+ name = "win-conpty-phase0"
683
+ version = "0.1.0"
684
+ dependencies = [
685
+ "anyhow",
686
+ "conpty-transport",
687
+ "serde",
688
+ "serde_json",
689
+ "windows",
690
+ ]
691
+
672
692
  [[package]]
673
693
  name = "winapi"
674
694
  version = "0.3.9"
@@ -691,6 +711,41 @@ version = "0.4.0"
691
711
  source = "registry+https://github.com/rust-lang/crates.io-index"
692
712
  checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
693
713
 
714
+ [[package]]
715
+ name = "windows"
716
+ version = "0.61.3"
717
+ source = "registry+https://github.com/rust-lang/crates.io-index"
718
+ checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
719
+ dependencies = [
720
+ "windows-collections",
721
+ "windows-core 0.61.2",
722
+ "windows-future",
723
+ "windows-link 0.1.3",
724
+ "windows-numerics",
725
+ ]
726
+
727
+ [[package]]
728
+ name = "windows-collections"
729
+ version = "0.2.0"
730
+ source = "registry+https://github.com/rust-lang/crates.io-index"
731
+ checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
732
+ dependencies = [
733
+ "windows-core 0.61.2",
734
+ ]
735
+
736
+ [[package]]
737
+ name = "windows-core"
738
+ version = "0.61.2"
739
+ source = "registry+https://github.com/rust-lang/crates.io-index"
740
+ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
741
+ dependencies = [
742
+ "windows-implement",
743
+ "windows-interface",
744
+ "windows-link 0.1.3",
745
+ "windows-result 0.3.4",
746
+ "windows-strings 0.4.2",
747
+ ]
748
+
694
749
  [[package]]
695
750
  name = "windows-core"
696
751
  version = "0.62.2"
@@ -699,9 +754,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
699
754
  dependencies = [
700
755
  "windows-implement",
701
756
  "windows-interface",
702
- "windows-link",
703
- "windows-result",
704
- "windows-strings",
757
+ "windows-link 0.2.1",
758
+ "windows-result 0.4.1",
759
+ "windows-strings 0.5.1",
760
+ ]
761
+
762
+ [[package]]
763
+ name = "windows-future"
764
+ version = "0.2.1"
765
+ source = "registry+https://github.com/rust-lang/crates.io-index"
766
+ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
767
+ dependencies = [
768
+ "windows-core 0.61.2",
769
+ "windows-link 0.1.3",
770
+ "windows-threading",
705
771
  ]
706
772
 
707
773
  [[package]]
@@ -726,19 +792,53 @@ dependencies = [
726
792
  "syn",
727
793
  ]
728
794
 
795
+ [[package]]
796
+ name = "windows-link"
797
+ version = "0.1.3"
798
+ source = "registry+https://github.com/rust-lang/crates.io-index"
799
+ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
800
+
729
801
  [[package]]
730
802
  name = "windows-link"
731
803
  version = "0.2.1"
732
804
  source = "registry+https://github.com/rust-lang/crates.io-index"
733
805
  checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
734
806
 
807
+ [[package]]
808
+ name = "windows-numerics"
809
+ version = "0.2.0"
810
+ source = "registry+https://github.com/rust-lang/crates.io-index"
811
+ checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
812
+ dependencies = [
813
+ "windows-core 0.61.2",
814
+ "windows-link 0.1.3",
815
+ ]
816
+
817
+ [[package]]
818
+ name = "windows-result"
819
+ version = "0.3.4"
820
+ source = "registry+https://github.com/rust-lang/crates.io-index"
821
+ checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
822
+ dependencies = [
823
+ "windows-link 0.1.3",
824
+ ]
825
+
735
826
  [[package]]
736
827
  name = "windows-result"
737
828
  version = "0.4.1"
738
829
  source = "registry+https://github.com/rust-lang/crates.io-index"
739
830
  checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
740
831
  dependencies = [
741
- "windows-link",
832
+ "windows-link 0.2.1",
833
+ ]
834
+
835
+ [[package]]
836
+ name = "windows-strings"
837
+ version = "0.4.2"
838
+ source = "registry+https://github.com/rust-lang/crates.io-index"
839
+ checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
840
+ dependencies = [
841
+ "windows-link 0.1.3",
742
842
  ]
743
843
 
744
844
  [[package]]
@@ -747,7 +847,16 @@ version = "0.5.1"
747
847
  source = "registry+https://github.com/rust-lang/crates.io-index"
748
848
  checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
749
849
  dependencies = [
750
- "windows-link",
850
+ "windows-link 0.2.1",
851
+ ]
852
+
853
+ [[package]]
854
+ name = "windows-threading"
855
+ version = "0.1.0"
856
+ source = "registry+https://github.com/rust-lang/crates.io-index"
857
+ checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
858
+ dependencies = [
859
+ "windows-link 0.1.3",
751
860
  ]
752
861
 
753
862
  [[package]]
package/Cargo.toml CHANGED
@@ -5,11 +5,11 @@
5
5
  # 等抽成独立成员 crate(届时各自顶上 §10 的 deny 锁 + cargo-deny ban)。
6
6
  [workspace]
7
7
  resolver = "2"
8
- members = ["crates/team-agent"]
8
+ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-transport"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.1"
12
+ version = "0.5.2"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -16,6 +16,7 @@ path = "src/main.rs"
16
16
  # step 2 (model) 已落地: serde/serde_json/thiserror/sha2。
17
17
  # rusqlite 仍留 workspace 调色板,step 3 (db) 再加(且需联网先 fetch)。
18
18
  [dependencies]
19
+ conpty-transport = { path = "../conpty-transport" }
19
20
  anyhow.workspace = true
20
21
  thiserror.workspace = true
21
22
  serde.workspace = true
@@ -0,0 +1,258 @@
1
+ use serde_json::{json, Value};
2
+ use std::io::{Read, Write};
3
+ use std::os::unix::net::{UnixListener, UnixStream};
4
+ use std::path::PathBuf;
5
+ use std::sync::{
6
+ atomic::{AtomicBool, Ordering},
7
+ Arc, Mutex,
8
+ };
9
+ use std::thread::JoinHandle;
10
+ use std::time::Duration;
11
+
12
+ #[derive(Clone)]
13
+ pub(crate) struct FakeAppServerScript {
14
+ pub user_agent: Option<String>,
15
+ pub thread_id: String,
16
+ pub session_id: String,
17
+ pub cwd: String,
18
+ pub resume_error: Option<String>,
19
+ pub turn_error: Option<String>,
20
+ pub turn_status: String,
21
+ }
22
+
23
+ impl FakeAppServerScript {
24
+ pub(crate) fn happy(thread_id: &str, session_id: &str, cwd: &str) -> Self {
25
+ Self {
26
+ user_agent: Some("codex-appserver-team-agent-test/0.139.0".to_string()),
27
+ thread_id: thread_id.to_string(),
28
+ session_id: session_id.to_string(),
29
+ cwd: cwd.to_string(),
30
+ resume_error: None,
31
+ turn_error: None,
32
+ turn_status: "inProgress".to_string(),
33
+ }
34
+ }
35
+ }
36
+
37
+ pub(crate) struct FakeAppServer {
38
+ endpoint: String,
39
+ path: PathBuf,
40
+ received_turns: Arc<Mutex<Vec<Value>>>,
41
+ running: Arc<AtomicBool>,
42
+ handle: Option<JoinHandle<()>>,
43
+ }
44
+
45
+ impl FakeAppServer {
46
+ pub(crate) fn start(tag: &str, script: FakeAppServerScript) -> Self {
47
+ static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
48
+ let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
49
+ let path = PathBuf::from(format!("/tmp/taas-{}-{id}-{tag}.sock", std::process::id()));
50
+ let _ = std::fs::remove_file(&path);
51
+ let listener = UnixListener::bind(&path).unwrap();
52
+ listener.set_nonblocking(true).unwrap();
53
+ let received_turns = Arc::new(Mutex::new(Vec::new()));
54
+ let running = Arc::new(AtomicBool::new(true));
55
+ let thread_turns = Arc::clone(&received_turns);
56
+ let thread_running = Arc::clone(&running);
57
+ let thread_path = path.clone();
58
+ let handle = std::thread::spawn(move || {
59
+ while thread_running.load(Ordering::Relaxed) {
60
+ match listener.accept() {
61
+ Ok((mut stream, _)) => {
62
+ let _ = handle_connection(&mut stream, &script, &thread_turns);
63
+ }
64
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
65
+ std::thread::sleep(Duration::from_millis(10));
66
+ }
67
+ Err(_) => break,
68
+ }
69
+ }
70
+ let _ = std::fs::remove_file(thread_path);
71
+ });
72
+ Self {
73
+ endpoint: format!("unix://{}", path.display()),
74
+ path,
75
+ received_turns,
76
+ running,
77
+ handle: Some(handle),
78
+ }
79
+ }
80
+
81
+ pub(crate) fn endpoint(&self) -> &str {
82
+ &self.endpoint
83
+ }
84
+
85
+ pub(crate) fn path(&self) -> &PathBuf {
86
+ &self.path
87
+ }
88
+
89
+ pub(crate) fn received_turns(&self) -> Vec<Value> {
90
+ self.received_turns.lock().unwrap().clone()
91
+ }
92
+ }
93
+
94
+ impl Drop for FakeAppServer {
95
+ fn drop(&mut self) {
96
+ self.running.store(false, Ordering::Relaxed);
97
+ let _ = UnixStream::connect(&self.path);
98
+ if let Some(handle) = self.handle.take() {
99
+ let _ = handle.join();
100
+ }
101
+ let _ = std::fs::remove_file(&self.path);
102
+ }
103
+ }
104
+
105
+ fn handle_connection(
106
+ stream: &mut UnixStream,
107
+ script: &FakeAppServerScript,
108
+ turns: &Arc<Mutex<Vec<Value>>>,
109
+ ) -> std::io::Result<()> {
110
+ stream.set_nonblocking(false)?;
111
+ stream.set_read_timeout(Some(Duration::from_secs(3)))?;
112
+ stream.set_write_timeout(Some(Duration::from_secs(3)))?;
113
+ read_http_upgrade(stream)?;
114
+ stream.write_all(
115
+ b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: test\r\n\r\n",
116
+ )?;
117
+ while let Some(frame) = read_ws_text(stream)? {
118
+ let value: Value = match serde_json::from_str(&frame) {
119
+ Ok(value) => value,
120
+ Err(_) => continue,
121
+ };
122
+ let method = value.get("method").and_then(Value::as_str).unwrap_or("");
123
+ let id = value.get("id").cloned().unwrap_or(Value::Null);
124
+ match method {
125
+ "initialize" => {
126
+ let mut result = serde_json::Map::new();
127
+ result.insert("codexHome".to_string(), json!("/tmp/codex-home"));
128
+ result.insert("platformFamily".to_string(), json!("unix"));
129
+ result.insert("platformOs".to_string(), json!("macos"));
130
+ if let Some(user_agent) = &script.user_agent {
131
+ result.insert("userAgent".to_string(), json!(user_agent));
132
+ }
133
+ write_ws_text(
134
+ stream,
135
+ &json!({"id": id, "result": Value::Object(result)}).to_string(),
136
+ )?;
137
+ }
138
+ "initialized" => {}
139
+ "thread/resume" => {
140
+ if let Some(message) = &script.resume_error {
141
+ write_ws_text(
142
+ stream,
143
+ &json!({"id": id, "error": {"code": -32600, "message": message}})
144
+ .to_string(),
145
+ )?;
146
+ } else {
147
+ write_ws_text(
148
+ stream,
149
+ &json!({
150
+ "id": id,
151
+ "result": {
152
+ "cwd": script.cwd,
153
+ "thread": {
154
+ "id": script.thread_id,
155
+ "sessionId": script.session_id,
156
+ "cwd": script.cwd,
157
+ "ephemeral": false
158
+ }
159
+ }
160
+ })
161
+ .to_string(),
162
+ )?;
163
+ }
164
+ }
165
+ "turn/start" => {
166
+ turns.lock().unwrap().push(value.clone());
167
+ if let Some(message) = &script.turn_error {
168
+ write_ws_text(
169
+ stream,
170
+ &json!({"id": id, "error": {"code": -32600, "message": message}})
171
+ .to_string(),
172
+ )?;
173
+ } else {
174
+ write_ws_text(
175
+ stream,
176
+ &json!({
177
+ "id": id,
178
+ "result": {
179
+ "turn": {"id": "turn-test", "status": script.turn_status}
180
+ }
181
+ })
182
+ .to_string(),
183
+ )?;
184
+ }
185
+ }
186
+ _ => {}
187
+ }
188
+ }
189
+ Ok(())
190
+ }
191
+
192
+ fn read_http_upgrade(stream: &mut UnixStream) -> std::io::Result<()> {
193
+ let mut data = Vec::new();
194
+ let mut buf = [0u8; 1];
195
+ while !data.ends_with(b"\r\n\r\n") {
196
+ stream.read_exact(&mut buf)?;
197
+ data.push(buf[0]);
198
+ }
199
+ Ok(())
200
+ }
201
+
202
+ fn read_ws_text(stream: &mut UnixStream) -> std::io::Result<Option<String>> {
203
+ let mut header = [0u8; 2];
204
+ match stream.read_exact(&mut header) {
205
+ Ok(()) => {}
206
+ Err(err)
207
+ if matches!(
208
+ err.kind(),
209
+ std::io::ErrorKind::UnexpectedEof
210
+ | std::io::ErrorKind::WouldBlock
211
+ | std::io::ErrorKind::TimedOut
212
+ ) =>
213
+ {
214
+ return Ok(None);
215
+ }
216
+ Err(err) => return Err(err),
217
+ }
218
+ let opcode = header[0] & 0x0f;
219
+ if opcode == 0x8 {
220
+ return Ok(None);
221
+ }
222
+ let masked = header[1] & 0x80 != 0;
223
+ let mut len = u64::from(header[1] & 0x7f);
224
+ if len == 126 {
225
+ let mut ext = [0u8; 2];
226
+ stream.read_exact(&mut ext)?;
227
+ len = u64::from(u16::from_be_bytes(ext));
228
+ } else if len == 127 {
229
+ let mut ext = [0u8; 8];
230
+ stream.read_exact(&mut ext)?;
231
+ len = u64::from_be_bytes(ext);
232
+ }
233
+ let mut mask = [0u8; 4];
234
+ if masked {
235
+ stream.read_exact(&mut mask)?;
236
+ }
237
+ let mut payload = vec![0u8; usize::try_from(len).unwrap_or(0)];
238
+ stream.read_exact(&mut payload)?;
239
+ if masked {
240
+ for (idx, byte) in payload.iter_mut().enumerate() {
241
+ *byte ^= mask[idx % 4];
242
+ }
243
+ }
244
+ Ok(Some(String::from_utf8_lossy(&payload).to_string()))
245
+ }
246
+
247
+ fn write_ws_text(stream: &mut UnixStream, text: &str) -> std::io::Result<()> {
248
+ let payload = text.as_bytes();
249
+ let mut frame = vec![0x81];
250
+ if payload.len() < 126 {
251
+ frame.push(payload.len() as u8);
252
+ } else {
253
+ frame.push(126);
254
+ frame.extend_from_slice(&(payload.len() as u16).to_be_bytes());
255
+ }
256
+ frame.extend_from_slice(payload);
257
+ stream.write_all(&frame)
258
+ }
@@ -107,6 +107,7 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
107
107
  args.team_id.as_deref(),
108
108
  args.yes,
109
109
  !args.no_display,
110
+ args.backend.as_deref(),
110
111
  )?;
111
112
  let readiness = value.get("readiness").and_then(Value::as_object);
112
113
  let all_resumable_have_session = readiness
@@ -716,8 +717,22 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
716
717
  .map_err(|e| CliError::Runtime(e.to_string()))?;
717
718
  let state = selected.state;
718
719
  let event_log = selected.run_workspace.join(".team").join("logs").join("events.jsonl");
719
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace);
720
- let (issues, suggested_repairs) = diagnose_runtime(&state, &backend);
720
+ // 0.5.x Phase 1d Batch 3: factory-routed diagnose backend so
721
+ // conpty teams get accurate `has_session` / capture from the shim
722
+ // rather than a workspace tmux fallback (which would always miss
723
+ // the ConPTY pane universe).
724
+ let backend: Box<dyn crate::transport::Transport> =
725
+ match crate::transport_factory::resolve_read_only_transport(
726
+ &selected.run_workspace,
727
+ Some(&state),
728
+ crate::transport_factory::TransportPurpose::Diagnose,
729
+ ) {
730
+ Ok(r) => r.backend,
731
+ Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
732
+ &selected.run_workspace,
733
+ )),
734
+ };
735
+ let (issues, suggested_repairs) = diagnose_runtime(&state, backend.as_ref());
721
736
  let ok = issues.as_array().is_some_and(Vec::is_empty);
722
737
  Ok(CmdResult::from_json(
723
738
  json!({
@@ -801,7 +816,21 @@ pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
801
816
  let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
802
817
  return Ok(peek_unavailable(&args.agent, args.json));
803
818
  };
804
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(&args.workspace);
819
+ // 0.5.x Phase 1d Batch 3: factory-routed peek backend so conpty
820
+ // teams peek from the shim scrollback rather than a workspace
821
+ // tmux capture that would always be empty. Tmux teams keep
822
+ // byte-equivalent behavior.
823
+ let backend: Box<dyn crate::transport::Transport> =
824
+ match crate::transport_factory::resolve_read_only_transport(
825
+ &args.workspace,
826
+ Some(&state),
827
+ crate::transport_factory::TransportPurpose::Status,
828
+ ) {
829
+ Ok(r) => r.backend,
830
+ Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
831
+ &args.workspace,
832
+ )),
833
+ };
805
834
  let windows = backend
806
835
  .list_windows(&crate::transport::SessionName::new(session.clone()))
807
836
  .map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -0,0 +1,16 @@
1
+ use super::*;
2
+
3
+ /// `team-agent attach-app-server-leader` writes a typed codex_app_server
4
+ /// leader_receiver through the leader lease primitive.
5
+ pub fn cmd_attach_app_server_leader(
6
+ args: &AttachAppServerLeaderArgs,
7
+ ) -> Result<CmdResult, CliError> {
8
+ let value = crate::leader::attach_app_server_leader(
9
+ &args.workspace,
10
+ args.team.as_deref(),
11
+ &args.socket,
12
+ &args.thread_id,
13
+ )
14
+ .map_err(|error| CliError::Runtime(error.to_string()))?;
15
+ Ok(CmdResult::from_json(value, args.json))
16
+ }
@@ -483,14 +483,23 @@ fn inject_tmux_session_present(workspace: &std::path::Path, state: &mut Value) {
483
483
  return;
484
484
  };
485
485
  let session_name_owned = session_name.to_string();
486
- let selection = crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
486
+ // 0.5.x Phase 1d Batch 3: factory-routed read path. conpty state
487
+ // hits the shim; tmux state hits the persisted endpoint (byte-
488
+ // equivalent). Diagnose no longer reports `tmux_session_missing`
489
+ // for a conpty team just because it can't find a tmux session on
490
+ // the workspace socket (design §Batch 3 Verification anchor).
491
+ let resolved = crate::transport_factory::resolve_read_only_transport(
487
492
  workspace,
488
493
  Some(state),
494
+ crate::transport_factory::TransportPurpose::Diagnose,
489
495
  );
490
- let present = selection
491
- .backend
492
- .has_session(&crate::transport::SessionName::new(session_name_owned))
493
- .unwrap_or(false);
496
+ let present = match resolved {
497
+ Ok(r) => r
498
+ .backend
499
+ .has_session(&crate::transport::SessionName::new(session_name_owned))
500
+ .unwrap_or(false),
501
+ Err(_) => false,
502
+ };
494
503
  if let Value::Object(map) = state {
495
504
  map.insert("tmux_session_present".to_string(), Value::Bool(present));
496
505
  }