@team-agent/installer 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +120 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +21 -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 +79 -1
- package/crates/team-agent/src/cli/mod.rs +190 -80
- 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 +135 -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 +549 -0
- package/crates/team-agent/src/conpty/backend.rs +822 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +132 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/diagnose/orphans.rs +29 -10
- 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/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +179 -0
- package/crates/team-agent/src/lib.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch.rs +207 -94
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -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/mcp_server/wire.rs +6 -7
- 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 +237 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/state/persist.rs +205 -25
- package/crates/team-agent/src/tmux_backend.rs +94 -13
- package/crates/team-agent/src/transport_factory.rs +751 -0
- package/package.json +4 -4
|
@@ -98,6 +98,8 @@ pub(crate) struct ResolvedNamedAddress {
|
|
|
98
98
|
pub session_name: Option<String>,
|
|
99
99
|
pub window_name: Option<String>,
|
|
100
100
|
pub tmux_endpoint: Option<String>,
|
|
101
|
+
pub transport_kind: Option<String>,
|
|
102
|
+
pub app_server: Option<Value>,
|
|
101
103
|
pub state_pane_id: Option<String>,
|
|
102
104
|
pub state_pane_stale: bool,
|
|
103
105
|
pub agent_status: Option<String>,
|
|
@@ -428,6 +430,8 @@ fn resolve_worker(
|
|
|
428
430
|
session_name: Some(session.to_string()),
|
|
429
431
|
window_name: Some(window.to_string()),
|
|
430
432
|
tmux_endpoint: transport.tmux_endpoint(),
|
|
433
|
+
transport_kind: Some("direct_tmux".to_string()),
|
|
434
|
+
app_server: None,
|
|
431
435
|
state_pane_id,
|
|
432
436
|
state_pane_stale,
|
|
433
437
|
agent_status,
|
|
@@ -541,6 +545,28 @@ fn resolve_leader(
|
|
|
541
545
|
state.get("leader_receiver")
|
|
542
546
|
}
|
|
543
547
|
.ok_or_else(|| name_not_live_leader(team, None, None, None, transport.tmux_endpoint()))?;
|
|
548
|
+
if let Some((mode, transport_kind)) = receiver_transport_conflict(receiver) {
|
|
549
|
+
let mut err = NamedAddressError::new(
|
|
550
|
+
NamedAddressErrorKind::NameNotLive,
|
|
551
|
+
"leader_receiver mode/transport_kind conflict",
|
|
552
|
+
);
|
|
553
|
+
err.action =
|
|
554
|
+
"Rebind the leader: team-agent claim-leader, takeover, or attach-app-server-leader"
|
|
555
|
+
.to_string();
|
|
556
|
+
err.log = format!(
|
|
557
|
+
"name={team}/leader mode={mode} transport_kind={transport_kind} expected=single_transport_kind"
|
|
558
|
+
);
|
|
559
|
+
return Err(err);
|
|
560
|
+
}
|
|
561
|
+
if crate::codex_app_server::receiver_is_app_server(receiver) {
|
|
562
|
+
return resolve_app_server_leader(
|
|
563
|
+
sender_workspace,
|
|
564
|
+
target_workspace,
|
|
565
|
+
team,
|
|
566
|
+
parsed,
|
|
567
|
+
receiver,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
544
570
|
let pane_id = string_field(receiver, "pane_id")
|
|
545
571
|
.filter(|pane| !pane.is_empty())
|
|
546
572
|
.ok_or_else(|| name_not_live_leader(team, None, None, None, transport.tmux_endpoint()))?;
|
|
@@ -566,6 +592,8 @@ fn resolve_leader(
|
|
|
566
592
|
session_name: session.map(str::to_string),
|
|
567
593
|
window_name: window.map(str::to_string),
|
|
568
594
|
tmux_endpoint: socket,
|
|
595
|
+
transport_kind: Some("direct_tmux".to_string()),
|
|
596
|
+
app_server: None,
|
|
569
597
|
state_pane_id: Some(pane_id.to_string()),
|
|
570
598
|
state_pane_stale: false,
|
|
571
599
|
agent_status: None,
|
|
@@ -597,6 +625,87 @@ fn resolve_leader(
|
|
|
597
625
|
}
|
|
598
626
|
}
|
|
599
627
|
|
|
628
|
+
fn receiver_transport_conflict(receiver: &Value) -> Option<(String, String)> {
|
|
629
|
+
let mode = receiver.get("mode").and_then(Value::as_str)?;
|
|
630
|
+
let transport_kind = receiver.get("transport_kind").and_then(Value::as_str)?;
|
|
631
|
+
if !mode.is_empty() && !transport_kind.is_empty() && mode != transport_kind {
|
|
632
|
+
return Some((mode.to_string(), transport_kind.to_string()));
|
|
633
|
+
}
|
|
634
|
+
None
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fn resolve_app_server_leader(
|
|
638
|
+
sender_workspace: &Path,
|
|
639
|
+
target_workspace: &Path,
|
|
640
|
+
team: &str,
|
|
641
|
+
parsed: &ParsedNamedAddress,
|
|
642
|
+
receiver: &Value,
|
|
643
|
+
) -> Result<ResolvedNamedAddress, NamedAddressError> {
|
|
644
|
+
let expected = crate::codex_app_server::binding_from_receiver(receiver).map_err(|error| {
|
|
645
|
+
let mut err = NamedAddressError::new(
|
|
646
|
+
NamedAddressErrorKind::NameNotLive,
|
|
647
|
+
format!("app-server leader receiver is incomplete: {error}"),
|
|
648
|
+
);
|
|
649
|
+
err.action = format!("Rebind the leader: team-agent attach-app-server-leader --team {team} --socket <socket> --thread-id <thread_id>");
|
|
650
|
+
err.log = format!("name={team}/leader expected=leader_receiver.app_server");
|
|
651
|
+
err
|
|
652
|
+
})?;
|
|
653
|
+
let actual = crate::codex_app_server::attach_probe(&expected.socket, &expected.thread_id)
|
|
654
|
+
.map_err(|error| {
|
|
655
|
+
let mut err = NamedAddressError::new(
|
|
656
|
+
NamedAddressErrorKind::NameNotLive,
|
|
657
|
+
format!("app-server leader thread is not live: {error}"),
|
|
658
|
+
);
|
|
659
|
+
err.action = format!("Rebind the leader: team-agent attach-app-server-leader --team {team} --socket {} --thread-id {}", expected.socket, expected.thread_id);
|
|
660
|
+
err.log = format!(
|
|
661
|
+
"name={team}/leader socket={} thread_id={} reason={}",
|
|
662
|
+
expected.socket,
|
|
663
|
+
expected.thread_id,
|
|
664
|
+
error
|
|
665
|
+
);
|
|
666
|
+
err
|
|
667
|
+
})?;
|
|
668
|
+
if actual.thread_id != expected.thread_id
|
|
669
|
+
|| actual.session_id != expected.session_id
|
|
670
|
+
|| actual.cwd != expected.cwd
|
|
671
|
+
|| actual.cli_version != expected.cli_version
|
|
672
|
+
{
|
|
673
|
+
let mut err = NamedAddressError::new(
|
|
674
|
+
NamedAddressErrorKind::NameNotLive,
|
|
675
|
+
"app-server leader thread identity tuple is stale",
|
|
676
|
+
);
|
|
677
|
+
err.action = format!("Rebind the leader: team-agent attach-app-server-leader --team {team} --socket {} --thread-id {}", expected.socket, expected.thread_id);
|
|
678
|
+
err.log = format!(
|
|
679
|
+
"name={team}/leader expected_thread={} actual_thread={} expected_session={} actual_session={} expected_cwd={} actual_cwd={}",
|
|
680
|
+
expected.thread_id,
|
|
681
|
+
actual.thread_id,
|
|
682
|
+
expected.session_id,
|
|
683
|
+
actual.session_id,
|
|
684
|
+
expected.cwd,
|
|
685
|
+
actual.cwd
|
|
686
|
+
);
|
|
687
|
+
return Err(err);
|
|
688
|
+
}
|
|
689
|
+
Ok(ResolvedNamedAddress {
|
|
690
|
+
raw_name: parsed.display_name(),
|
|
691
|
+
target_kind: NamedTargetKind::Leader,
|
|
692
|
+
sender_workspace: sender_workspace.to_path_buf(),
|
|
693
|
+
target_workspace: target_workspace.to_path_buf(),
|
|
694
|
+
team_key: Some(team.to_string()),
|
|
695
|
+
agent_id: None,
|
|
696
|
+
pane_id: String::new(),
|
|
697
|
+
session_name: None,
|
|
698
|
+
window_name: None,
|
|
699
|
+
tmux_endpoint: None,
|
|
700
|
+
transport_kind: Some("codex_app_server".to_string()),
|
|
701
|
+
app_server: receiver.get("app_server").cloned(),
|
|
702
|
+
state_pane_id: None,
|
|
703
|
+
state_pane_stale: false,
|
|
704
|
+
agent_status: None,
|
|
705
|
+
warning: None,
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
|
|
600
709
|
fn resolve_session_window(
|
|
601
710
|
sender_workspace: &Path,
|
|
602
711
|
target_workspace: &Path,
|
|
@@ -619,6 +728,8 @@ fn resolve_session_window(
|
|
|
619
728
|
session_name: Some(session.to_string()),
|
|
620
729
|
window_name: Some(window.to_string()),
|
|
621
730
|
tmux_endpoint: transport.tmux_endpoint(),
|
|
731
|
+
transport_kind: Some("direct_tmux".to_string()),
|
|
732
|
+
app_server: None,
|
|
622
733
|
state_pane_id: None,
|
|
623
734
|
state_pane_stale: false,
|
|
624
735
|
agent_status: None,
|
|
@@ -17,7 +17,9 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
17
17
|
}
|
|
18
18
|
let content = args.message.join(" ");
|
|
19
19
|
if content.is_empty() {
|
|
20
|
-
return Err(CliError::Usage(
|
|
20
|
+
return Err(CliError::Usage(
|
|
21
|
+
"--to-name requires a non-empty message".to_string(),
|
|
22
|
+
));
|
|
21
23
|
}
|
|
22
24
|
let (resolved, transport) =
|
|
23
25
|
match crate::cli::named_address::resolve_name_for_cli(&args.workspace, to_name) {
|
|
@@ -52,7 +54,9 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
52
54
|
}
|
|
53
55
|
let content = args.message.join(" ");
|
|
54
56
|
if content.is_empty() {
|
|
55
|
-
return Err(CliError::Usage(
|
|
57
|
+
return Err(CliError::Usage(
|
|
58
|
+
"--pane requires a non-empty message".to_string(),
|
|
59
|
+
));
|
|
56
60
|
}
|
|
57
61
|
let mut value = send_to_pane_direct(
|
|
58
62
|
&args.workspace,
|
|
@@ -191,13 +195,22 @@ fn send_to_named_pane_direct(
|
|
|
191
195
|
|
|
192
196
|
let message_id = format!("named_send_{}", chrono::Utc::now().timestamp_millis());
|
|
193
197
|
let rendered = render_message(sender, task_id, content, &message_id);
|
|
194
|
-
let target = Target::Pane(PaneId::new(&resolved.pane_id));
|
|
195
198
|
let sender_run_workspace = crate::model::paths::canonical_run_workspace(sender_workspace)
|
|
196
199
|
.unwrap_or_else(|_| sender_workspace.to_path_buf());
|
|
197
200
|
let event_log = crate::event_log::EventLog::new(&sender_run_workspace);
|
|
198
201
|
if let Some(warning) = &resolved.warning {
|
|
199
202
|
eprintln!("warning: {warning}");
|
|
200
203
|
}
|
|
204
|
+
if resolved.transport_kind.as_deref() == Some("codex_app_server") {
|
|
205
|
+
return send_to_named_app_server_leader(
|
|
206
|
+
&event_log,
|
|
207
|
+
resolved,
|
|
208
|
+
&message_id,
|
|
209
|
+
&rendered,
|
|
210
|
+
sender,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
let target = Target::Pane(PaneId::new(&resolved.pane_id));
|
|
201
214
|
let report = transport
|
|
202
215
|
.inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
|
|
203
216
|
.map_err(|e| {
|
|
@@ -241,6 +254,88 @@ fn send_to_named_pane_direct(
|
|
|
241
254
|
Ok(value)
|
|
242
255
|
}
|
|
243
256
|
|
|
257
|
+
fn send_to_named_app_server_leader(
|
|
258
|
+
event_log: &crate::event_log::EventLog,
|
|
259
|
+
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
260
|
+
message_id: &str,
|
|
261
|
+
rendered: &str,
|
|
262
|
+
sender: &str,
|
|
263
|
+
) -> Result<serde_json::Value, CliError> {
|
|
264
|
+
let receiver = serde_json::json!({
|
|
265
|
+
"mode": "codex_app_server",
|
|
266
|
+
"transport_kind": "codex_app_server",
|
|
267
|
+
"app_server": resolved.app_server.clone().unwrap_or(serde_json::Value::Null),
|
|
268
|
+
});
|
|
269
|
+
let binding = crate::codex_app_server::binding_from_receiver(&receiver)
|
|
270
|
+
.map_err(|error| CliError::Runtime(format!("invalid app-server named leader: {error}")))?;
|
|
271
|
+
let target_kind = named_target_kind_wire(resolved.target_kind);
|
|
272
|
+
let base_event = serde_json::json!({
|
|
273
|
+
"to_name": resolved.raw_name,
|
|
274
|
+
"target_kind": target_kind,
|
|
275
|
+
"sender": sender,
|
|
276
|
+
"sender_workspace": resolved.sender_workspace.display().to_string(),
|
|
277
|
+
"target_workspace": resolved.target_workspace.display().to_string(),
|
|
278
|
+
"team_key": resolved.team_key,
|
|
279
|
+
"agent_id": resolved.agent_id,
|
|
280
|
+
"transport_kind": "codex_app_server",
|
|
281
|
+
"socket": binding.socket,
|
|
282
|
+
"thread_id": binding.thread_id,
|
|
283
|
+
"message_id": message_id,
|
|
284
|
+
});
|
|
285
|
+
match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, rendered) {
|
|
286
|
+
Ok(submit) => {
|
|
287
|
+
let event = merge_json(
|
|
288
|
+
base_event.clone(),
|
|
289
|
+
serde_json::json!({
|
|
290
|
+
"ok": true,
|
|
291
|
+
"turn_id": submit.turn_id,
|
|
292
|
+
"turn_status": submit.turn_status,
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
let _ = event_log.write("send.name_app_server", event.clone());
|
|
296
|
+
Ok(event)
|
|
297
|
+
}
|
|
298
|
+
Err(crate::codex_app_server::AppServerError::LeaderBusy(message)) => {
|
|
299
|
+
let event = merge_json(
|
|
300
|
+
base_event.clone(),
|
|
301
|
+
serde_json::json!({
|
|
302
|
+
"ok": false,
|
|
303
|
+
"status": "retry_scheduled",
|
|
304
|
+
"reason": "leader_busy",
|
|
305
|
+
"channel": "leader_busy",
|
|
306
|
+
"error": message,
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
let _ = event_log.write("send.name_app_server", event.clone());
|
|
310
|
+
Ok(event)
|
|
311
|
+
}
|
|
312
|
+
Err(error) => {
|
|
313
|
+
let event = merge_json(
|
|
314
|
+
base_event.clone(),
|
|
315
|
+
serde_json::json!({
|
|
316
|
+
"ok": false,
|
|
317
|
+
"status": "refused",
|
|
318
|
+
"reason": error.code(),
|
|
319
|
+
"channel": "rebind_required",
|
|
320
|
+
"error": error.to_string(),
|
|
321
|
+
"action": "run team-agent attach-app-server-leader for the target team",
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
let _ = event_log.write("send.name_app_server", event.clone());
|
|
325
|
+
Ok(event)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn merge_json(mut left: serde_json::Value, right: serde_json::Value) -> serde_json::Value {
|
|
331
|
+
if let (Some(left), Some(right)) = (left.as_object_mut(), right.as_object()) {
|
|
332
|
+
for (key, value) in right {
|
|
333
|
+
left.insert(key.clone(), value.clone());
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
left
|
|
337
|
+
}
|
|
338
|
+
|
|
244
339
|
fn named_target_kind_wire(kind: crate::cli::named_address::NamedTargetKind) -> &'static str {
|
|
245
340
|
match kind {
|
|
246
341
|
crate::cli::named_address::NamedTargetKind::Worker => "worker",
|
|
@@ -280,7 +280,29 @@ use rusqlite::params;
|
|
|
280
280
|
if let (Some(session), Some(agents)) = (session, state.get("agents").and_then(Value::as_object)) {
|
|
281
281
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
282
282
|
.unwrap_or_else(|_| workspace.to_path_buf());
|
|
283
|
-
|
|
283
|
+
// 0.5.x Phase 1d Batch 3: use the factory-resolved backend
|
|
284
|
+
// so conpty teams get their scrollback from the shim rather
|
|
285
|
+
// than a fake tmux capture that always returns empty. Tmux
|
|
286
|
+
// teams take the same code path as before (byte-equivalent).
|
|
287
|
+
let resolved = crate::transport_factory::resolve_read_only_transport(
|
|
288
|
+
&run_ws,
|
|
289
|
+
Some(state),
|
|
290
|
+
crate::transport_factory::TransportPurpose::Status,
|
|
291
|
+
);
|
|
292
|
+
let backend: Box<dyn crate::transport::Transport> = match resolved {
|
|
293
|
+
Ok(r) => r.backend,
|
|
294
|
+
Err(_) => {
|
|
295
|
+
// Read-path fallback: refused factory means we don't
|
|
296
|
+
// try to inspect approval prompts. Empty vec = no
|
|
297
|
+
// waiting approvals, honest.
|
|
298
|
+
return Ok(json!({
|
|
299
|
+
"ok": true,
|
|
300
|
+
"waiting": false,
|
|
301
|
+
"waiting_count": 0,
|
|
302
|
+
"approvals": [],
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
284
306
|
for (agent_id, agent_state) in agents {
|
|
285
307
|
if agent.is_some_and(|wanted| wanted != agent_id) {
|
|
286
308
|
continue;
|
|
@@ -469,14 +491,30 @@ use rusqlite::params;
|
|
|
469
491
|
}
|
|
470
492
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
471
493
|
.unwrap_or_else(|_| workspace.to_path_buf());
|
|
472
|
-
|
|
494
|
+
// 0.5.x Phase 1d Batch 3: route through the factory so a
|
|
495
|
+
// conpty team does NOT get its `has_session` probe served by a
|
|
496
|
+
// tmux backend (which would always return false and drive the
|
|
497
|
+
// reader into a false `tmux_session_missing` state — design
|
|
498
|
+
// §Batch 3 Verification anchor). Tmux teams see byte-equivalent
|
|
499
|
+
// behavior because factory Layer 3 (legacy tmux endpoint) uses
|
|
500
|
+
// the same `tmux_backend_for_runtime_state_or_workspace` shape.
|
|
501
|
+
let resolved = crate::transport_factory::resolve_read_only_transport(
|
|
473
502
|
&run_ws,
|
|
474
503
|
Some(state),
|
|
504
|
+
crate::transport_factory::TransportPurpose::Status,
|
|
475
505
|
);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
506
|
+
match resolved {
|
|
507
|
+
Ok(r) => r
|
|
508
|
+
.backend
|
|
509
|
+
.has_session(&crate::transport::SessionName::new(name))
|
|
510
|
+
.unwrap_or(false),
|
|
511
|
+
Err(_) => {
|
|
512
|
+
// Factory refused (e.g. explicit conpty without a
|
|
513
|
+
// resolvable team_key). Honest: return false rather
|
|
514
|
+
// than pretend a tmux session exists.
|
|
515
|
+
false
|
|
516
|
+
}
|
|
517
|
+
}
|
|
480
518
|
}
|
|
481
519
|
|
|
482
520
|
fn message_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
|
|
@@ -75,6 +75,38 @@ fn leader_team(session: &str, pane: &str, socket: &str) -> serde_json::Value {
|
|
|
75
75
|
})
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
fn app_server_leader_team(
|
|
79
|
+
ws: &Path,
|
|
80
|
+
endpoint: &str,
|
|
81
|
+
thread_id: &str,
|
|
82
|
+
session_id: &str,
|
|
83
|
+
) -> serde_json::Value {
|
|
84
|
+
json!({
|
|
85
|
+
"session_name": "alpha-session",
|
|
86
|
+
"agents": {},
|
|
87
|
+
"leader_receiver": {
|
|
88
|
+
"mode": "codex_app_server",
|
|
89
|
+
"transport_kind": "codex_app_server",
|
|
90
|
+
"status": "attached",
|
|
91
|
+
"provider": "codex",
|
|
92
|
+
"owner_epoch": 3,
|
|
93
|
+
"app_server": {
|
|
94
|
+
"socket": endpoint,
|
|
95
|
+
"thread_id": thread_id,
|
|
96
|
+
"session_id": session_id,
|
|
97
|
+
"cwd": ws.to_string_lossy(),
|
|
98
|
+
"cli_version": "codex-appserver-team-agent-test/0.139.0",
|
|
99
|
+
"bound_at": "2026-07-05T00:00:00Z"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"team_owner": {
|
|
103
|
+
"provider": "codex",
|
|
104
|
+
"transport_kind": "codex_app_server",
|
|
105
|
+
"owner_epoch": 3
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
fn pane(session: &str, window: &str, pane_id: &str) -> PaneInfo {
|
|
79
111
|
PaneInfo {
|
|
80
112
|
pane_id: PaneId::new(pane_id),
|
|
@@ -323,6 +355,109 @@ fn resolve_leader_name_after_rebind() {
|
|
|
323
355
|
let _ = std::fs::remove_dir_all(&ws);
|
|
324
356
|
}
|
|
325
357
|
|
|
358
|
+
// 0.5.x Windows portability Batch 5: `FakeAppServer` uses Unix
|
|
359
|
+
// domain sockets — Unix-only.
|
|
360
|
+
#[cfg(unix)]
|
|
361
|
+
#[test]
|
|
362
|
+
fn resolve_app_server_leader_name_uses_typed_receiver_not_tmux_pane() {
|
|
363
|
+
let ws = named_ws("appserver-leader-resolve");
|
|
364
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
365
|
+
"named-resolve",
|
|
366
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
367
|
+
"thread-live",
|
|
368
|
+
"session-live",
|
|
369
|
+
ws.to_str().unwrap(),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
seed_state(
|
|
373
|
+
&ws,
|
|
374
|
+
state_with_teams(json!({
|
|
375
|
+
"alpha": app_server_leader_team(&ws, fake.endpoint(), "thread-live", "session-live")
|
|
376
|
+
})),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
let resolved = resolve_name_with_transport(&ws, "alpha/leader", &OfflineTransport::new())
|
|
380
|
+
.expect("app-server leader name resolves by typed receiver");
|
|
381
|
+
|
|
382
|
+
assert_eq!(resolved.target_kind, NamedTargetKind::Leader);
|
|
383
|
+
assert_eq!(resolved.transport_kind.as_deref(), Some("codex_app_server"));
|
|
384
|
+
assert!(
|
|
385
|
+
resolved.pane_id.is_empty(),
|
|
386
|
+
"app-server leader is not pane-addressed"
|
|
387
|
+
);
|
|
388
|
+
assert_eq!(
|
|
389
|
+
resolved
|
|
390
|
+
.app_server
|
|
391
|
+
.as_ref()
|
|
392
|
+
.and_then(|app| app.get("thread_id"))
|
|
393
|
+
.and_then(serde_json::Value::as_str),
|
|
394
|
+
Some("thread-live")
|
|
395
|
+
);
|
|
396
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn resolve_app_server_leader_name_fails_closed_on_transport_conflict() {
|
|
401
|
+
let ws = named_ws("appserver-leader-conflict");
|
|
402
|
+
let mut team = app_server_leader_team(
|
|
403
|
+
&ws,
|
|
404
|
+
"unix:///tmp/team-agent-should-not-connect.sock",
|
|
405
|
+
"thread-live",
|
|
406
|
+
"session-live",
|
|
407
|
+
);
|
|
408
|
+
team["leader_receiver"]["mode"] = json!("direct_tmux");
|
|
409
|
+
seed_state(
|
|
410
|
+
&ws,
|
|
411
|
+
state_with_teams(json!({
|
|
412
|
+
"alpha": team
|
|
413
|
+
})),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
let err = resolve_name_with_transport(&ws, "alpha/leader", &OfflineTransport::new())
|
|
417
|
+
.expect_err("conflicting mode/transport_kind must fail closed");
|
|
418
|
+
|
|
419
|
+
assert_eq!(err.kind, NamedAddressErrorKind::NameNotLive);
|
|
420
|
+
assert!(err.log.contains("mode=direct_tmux"), "{err}");
|
|
421
|
+
assert!(err.log.contains("transport_kind=codex_app_server"), "{err}");
|
|
422
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#[cfg(unix)]
|
|
426
|
+
#[test]
|
|
427
|
+
fn send_to_name_app_server_leader_submits_turn_without_tmux_pane() {
|
|
428
|
+
let ws = named_ws("appserver-leader-send");
|
|
429
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
430
|
+
"named-send",
|
|
431
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
432
|
+
"thread-live",
|
|
433
|
+
"session-live",
|
|
434
|
+
ws.to_str().unwrap(),
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
seed_state(
|
|
438
|
+
&ws,
|
|
439
|
+
state_with_teams(json!({
|
|
440
|
+
"alpha": app_server_leader_team(&ws, fake.endpoint(), "thread-live", "session-live")
|
|
441
|
+
})),
|
|
442
|
+
);
|
|
443
|
+
let args = named_send_args(&ws, Some("alpha/leader"), None, None, &["hello appserver"]);
|
|
444
|
+
|
|
445
|
+
let result = cmd_send(&args).expect("named app-server send should succeed");
|
|
446
|
+
let value = match result.output {
|
|
447
|
+
CmdOutput::Json(value) => value,
|
|
448
|
+
other => panic!("expected json output, got {other:?}"),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
assert_eq!(value["ok"], json!(true));
|
|
452
|
+
assert_eq!(value["transport_kind"], json!("codex_app_server"));
|
|
453
|
+
assert_eq!(fake.received_turns().len(), 1);
|
|
454
|
+
assert_eq!(
|
|
455
|
+
fake.received_turns()[0]["params"]["clientUserMessageId"],
|
|
456
|
+
value["message_id"]
|
|
457
|
+
);
|
|
458
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
459
|
+
}
|
|
460
|
+
|
|
326
461
|
#[test]
|
|
327
462
|
fn resolve_name_invalid_syntax() {
|
|
328
463
|
let ws = named_ws("invalid");
|
|
@@ -206,6 +206,7 @@ fn current_uid() -> Option<String> {
|
|
|
206
206
|
team_id: None,
|
|
207
207
|
yes: true,
|
|
208
208
|
no_display: false,
|
|
209
|
+
backend: None,
|
|
209
210
|
json: true,
|
|
210
211
|
};
|
|
211
212
|
let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
|
|
@@ -240,6 +241,7 @@ fn current_uid() -> Option<String> {
|
|
|
240
241
|
team_id: None,
|
|
241
242
|
yes: false,
|
|
242
243
|
no_display: false,
|
|
244
|
+
backend: None,
|
|
243
245
|
json: true,
|
|
244
246
|
};
|
|
245
247
|
let text = outcome_text(cmd_quick_start(&args));
|
|
@@ -272,6 +272,13 @@ pub struct QuickStartArgs {
|
|
|
272
272
|
pub yes: bool,
|
|
273
273
|
pub no_display: bool,
|
|
274
274
|
pub json: bool,
|
|
275
|
+
/// 0.5.x Phase 1d Batch 2: explicit backend override
|
|
276
|
+
/// (`--backend <tmux|conpty>`). `None` = default (factory 5-layer
|
|
277
|
+
/// resolution; almost always `tmux` on a cold quick-start).
|
|
278
|
+
/// Explicit `Some("conpty")` on a host without a live shim client
|
|
279
|
+
/// surfaces `MuxUnavailable` honestly (CR C-1 ①) — NEVER silent
|
|
280
|
+
/// downgrade to tmux. Explicit `Some("pty")` refuses (CR C-1 ②).
|
|
281
|
+
pub backend: Option<String>,
|
|
275
282
|
// Stage QR (quick-start/restart separation, design doc
|
|
276
283
|
// .team/artifacts/quickstart-restart-separation-design.md): the
|
|
277
284
|
// `fresh` field is intentionally REMOVED. Reset semantics now live
|
|
@@ -440,6 +447,16 @@ pub struct AttachLeaderArgs {
|
|
|
440
447
|
pub json: bool,
|
|
441
448
|
}
|
|
442
449
|
|
|
450
|
+
/// Explicit Codex app-server leader binding args.
|
|
451
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
452
|
+
pub struct AttachAppServerLeaderArgs {
|
|
453
|
+
pub workspace: PathBuf,
|
|
454
|
+
pub team: Option<String>,
|
|
455
|
+
pub socket: String,
|
|
456
|
+
pub thread_id: String,
|
|
457
|
+
pub json: bool,
|
|
458
|
+
}
|
|
459
|
+
|
|
443
460
|
/// `identity`(`parser.py:256`)。
|
|
444
461
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
445
462
|
pub struct IdentityArgs {
|