@team-agent/installer 0.5.2 → 0.5.4
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 +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +213 -89
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- 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/coordinator/tick.rs +13 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +89 -92
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -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/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +222 -25
- package/crates/team-agent/src/state/projection.rs +59 -4
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- package/package.json +4 -4
|
@@ -210,14 +210,22 @@ pub mod lifecycle_port {
|
|
|
210
210
|
/// `&*box`. Factory refusal falls back to the workspace tmux
|
|
211
211
|
/// backend byte-equivalent to today so daemon liveness paths
|
|
212
212
|
/// don't crash.
|
|
213
|
-
pub fn shutdown(
|
|
213
|
+
pub fn shutdown(
|
|
214
|
+
workspace: &Path,
|
|
215
|
+
keep_logs: bool,
|
|
216
|
+
team: Option<&str>,
|
|
217
|
+
) -> Result<Value, CliError> {
|
|
214
218
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
215
219
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
216
220
|
let state = shutdown_state_for_team(&run_ws, team)?;
|
|
217
221
|
if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
|
|
218
222
|
let transport = shutdown_transport_for_endpoint(endpoint);
|
|
219
223
|
return shutdown_with_transport_and_state(
|
|
220
|
-
workspace,
|
|
224
|
+
workspace,
|
|
225
|
+
keep_logs,
|
|
226
|
+
team,
|
|
227
|
+
&transport,
|
|
228
|
+
Some(state),
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
let boxed: Box<dyn crate::transport::Transport> =
|
|
@@ -352,11 +360,7 @@ pub mod lifecycle_port {
|
|
|
352
360
|
let anchor_sessions = anchor_sessions_from_state(state, &pane_targets, event_log);
|
|
353
361
|
match sessions_to_kill(&sessions, &anchor_sessions) {
|
|
354
362
|
KillDecision::KillServerExclusive => {
|
|
355
|
-
if state
|
|
356
|
-
.get("tmux_socket_source")
|
|
357
|
-
.and_then(Value::as_str)
|
|
358
|
-
== Some("leader_env")
|
|
359
|
-
{
|
|
363
|
+
if state.get("tmux_socket_source").and_then(Value::as_str) == Some("leader_env") {
|
|
360
364
|
let _ = event_log.write(
|
|
361
365
|
"shutdown.kill_server_skipped_shared_socket",
|
|
362
366
|
json!({
|
|
@@ -463,10 +467,7 @@ pub mod lifecycle_port {
|
|
|
463
467
|
.cloned()
|
|
464
468
|
.collect::<Vec<_>>();
|
|
465
469
|
if let Some(anchored) = target_spared_for_anchor {
|
|
466
|
-
if !spared
|
|
467
|
-
.iter()
|
|
468
|
-
.any(|s| s.as_str() == anchored.as_str())
|
|
469
|
-
{
|
|
470
|
+
if !spared.iter().any(|s| s.as_str() == anchored.as_str()) {
|
|
470
471
|
spared.push(anchored);
|
|
471
472
|
}
|
|
472
473
|
}
|
|
@@ -486,7 +487,10 @@ pub mod lifecycle_port {
|
|
|
486
487
|
// (this function) computes its own `to_kill` from `target`. Any
|
|
487
488
|
// leader-prefixed entry here is a topology violation introduced
|
|
488
489
|
// somewhere upstream. Log loudly and skip the kill.
|
|
489
|
-
if session
|
|
490
|
+
if session
|
|
491
|
+
.as_str()
|
|
492
|
+
.starts_with(crate::leader::LEADER_SESSION_PREFIX)
|
|
493
|
+
{
|
|
490
494
|
eprintln!(
|
|
491
495
|
"team_agent::layout shutdown_invariant_violation kind=KillListContainsLeaderSession \
|
|
492
496
|
session=`{}` action=skipping_kill (post-Step-9 will hard-fail)",
|
|
@@ -723,11 +727,7 @@ pub mod lifecycle_port {
|
|
|
723
727
|
if crate::state::projection::state_is_managed_leader(&state)
|
|
724
728
|
&& session_has_leader_anchor
|
|
725
729
|
{
|
|
726
|
-
let _ = event_log_write_session_spared(
|
|
727
|
-
&run_workspace,
|
|
728
|
-
session,
|
|
729
|
-
&leader_anchor_ids,
|
|
730
|
-
);
|
|
730
|
+
let _ = event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
|
|
731
731
|
push_unique_session(&mut spared_sessions, session.clone());
|
|
732
732
|
let worker_panes = collect_session_worker_panes(
|
|
733
733
|
&state,
|
|
@@ -742,10 +742,8 @@ pub mod lifecycle_port {
|
|
|
742
742
|
}
|
|
743
743
|
}
|
|
744
744
|
}
|
|
745
|
-
let _ =
|
|
746
|
-
&run_workspace,
|
|
747
|
-
session,
|
|
748
|
-
);
|
|
745
|
+
let _ =
|
|
746
|
+
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
749
747
|
} else {
|
|
750
748
|
push_unique_session(&mut killed_sessions, session.clone());
|
|
751
749
|
if let Err(error) = transport.kill_session(session) {
|
|
@@ -753,10 +751,8 @@ pub mod lifecycle_port {
|
|
|
753
751
|
kill_error = Some(error.to_string());
|
|
754
752
|
}
|
|
755
753
|
}
|
|
756
|
-
let _ =
|
|
757
|
-
&run_workspace,
|
|
758
|
-
session,
|
|
759
|
-
);
|
|
754
|
+
let _ =
|
|
755
|
+
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
760
756
|
}
|
|
761
757
|
}
|
|
762
758
|
deadline.check("reap_workspace_residuals")?;
|
|
@@ -943,7 +939,17 @@ pub mod lifecycle_port {
|
|
|
943
939
|
}),
|
|
944
940
|
)
|
|
945
941
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
946
|
-
|
|
942
|
+
// 0.5.x Windows portability Batch 6 Option A (leader
|
|
943
|
+
// constraint 3): if a ConPTY shim was recorded in
|
|
944
|
+
// `state.transport.shim.pid`, terminate it explicitly via
|
|
945
|
+
// `platform::process::terminate_pid`.
|
|
946
|
+
//
|
|
947
|
+
// Batch 7 refinement: the field is emitted into the shutdown
|
|
948
|
+
// JSON only on Windows so Unix golden fixtures stay
|
|
949
|
+
// byte-preserving. The Unix behavior is a no-op (no shim
|
|
950
|
+
// concept there); adding a placeholder key would just noise
|
|
951
|
+
// up the fixtures.
|
|
952
|
+
let response = json!({
|
|
947
953
|
"ok": ok,
|
|
948
954
|
"status": status,
|
|
949
955
|
"phase": phase,
|
|
@@ -966,8 +972,61 @@ pub mod lifecycle_port {
|
|
|
966
972
|
"coordinator": {
|
|
967
973
|
"status": coordinator_status,
|
|
968
974
|
"pid": coordinator_pid,
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
#[cfg(windows)]
|
|
978
|
+
let mut response = response;
|
|
979
|
+
#[cfg(windows)]
|
|
980
|
+
{
|
|
981
|
+
if let Some(obj) = response.as_object_mut() {
|
|
982
|
+
obj.insert(
|
|
983
|
+
"conpty_shim".to_string(),
|
|
984
|
+
shutdown_conpty_shim(&run_workspace),
|
|
985
|
+
);
|
|
969
986
|
}
|
|
970
|
-
}
|
|
987
|
+
}
|
|
988
|
+
#[cfg(not(windows))]
|
|
989
|
+
{
|
|
990
|
+
let _ = &run_workspace;
|
|
991
|
+
}
|
|
992
|
+
Ok(response)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/// 0.5.x Windows portability Batch 6 Option A: locate and
|
|
996
|
+
/// terminate the ConPTY shim if `state.transport.shim.pid` is
|
|
997
|
+
/// recorded. Returns a JSON blob for the shutdown response so
|
|
998
|
+
/// operators can distinguish "no shim was ever launched" from
|
|
999
|
+
/// "shim terminated cleanly" from "shim already gone".
|
|
1000
|
+
///
|
|
1001
|
+
/// Windows-only; caller (`shutdown_with_transport_and_state`)
|
|
1002
|
+
/// only invokes on Windows. Unix has no shim concept.
|
|
1003
|
+
#[cfg(windows)]
|
|
1004
|
+
fn shutdown_conpty_shim(workspace: &Path) -> serde_json::Value {
|
|
1005
|
+
let Some(pid) = crate::coordinator::conpty_shim::recorded_shim_pid(workspace) else {
|
|
1006
|
+
return json!({ "action": "no_shim_recorded" });
|
|
1007
|
+
};
|
|
1008
|
+
// Prefer graceful termination — Windows maps this to
|
|
1009
|
+
// `TerminationOutcome::ForceOnly` (Batch 3 anchor: no
|
|
1010
|
+
// SIGTERM equivalent for non-console child), which
|
|
1011
|
+
// surfaces the honest audit signal in the JSON.
|
|
1012
|
+
let outcome = crate::platform::process::terminate_pid(
|
|
1013
|
+
pid,
|
|
1014
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1015
|
+
);
|
|
1016
|
+
match outcome {
|
|
1017
|
+
Ok(crate::platform::process::TerminationOutcome::Requested) => {
|
|
1018
|
+
json!({ "pid": pid, "action": "terminated" })
|
|
1019
|
+
}
|
|
1020
|
+
Ok(crate::platform::process::TerminationOutcome::AlreadyGone) => {
|
|
1021
|
+
json!({ "pid": pid, "action": "already_gone" })
|
|
1022
|
+
}
|
|
1023
|
+
Ok(crate::platform::process::TerminationOutcome::ForceOnly { reason }) => {
|
|
1024
|
+
json!({ "pid": pid, "action": "terminated_force_only", "reason": reason })
|
|
1025
|
+
}
|
|
1026
|
+
Err(e) => {
|
|
1027
|
+
json!({ "pid": pid, "action": "terminate_failed", "reason": e.to_string() })
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
971
1030
|
}
|
|
972
1031
|
|
|
973
1032
|
/// T5 (harvest §1 / A2): the bounded stop RETAINS the JoinHandle and reclaims the
|
|
@@ -1230,10 +1289,7 @@ pub mod lifecycle_port {
|
|
|
1230
1289
|
// probes when the primary transport is ConPTY; the primary
|
|
1231
1290
|
// transport check above already covered the honest question
|
|
1232
1291
|
// ("does the shim still have this session?").
|
|
1233
|
-
let primary_is_conpty = matches!(
|
|
1234
|
-
transport.kind(),
|
|
1235
|
-
crate::transport::BackendKind::ConPty
|
|
1236
|
-
);
|
|
1292
|
+
let primary_is_conpty = matches!(transport.kind(), crate::transport::BackendKind::ConPty);
|
|
1237
1293
|
if !primary_is_conpty {
|
|
1238
1294
|
let workspace_transport = shutdown_workspace_transport(workspace);
|
|
1239
1295
|
match crate::transport::Transport::has_session(&workspace_transport, session) {
|
|
@@ -1353,34 +1409,48 @@ pub mod lifecycle_port {
|
|
|
1353
1409
|
return;
|
|
1354
1410
|
}
|
|
1355
1411
|
for pid in pids.iter().rev() {
|
|
1356
|
-
|
|
1412
|
+
let _ = crate::platform::process::terminate_pid(
|
|
1413
|
+
*pid,
|
|
1414
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
1415
|
+
);
|
|
1357
1416
|
}
|
|
1358
1417
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1359
1418
|
for pid in pids.iter().rev() {
|
|
1360
|
-
|
|
1419
|
+
let _ = crate::platform::process::terminate_pid(
|
|
1420
|
+
*pid,
|
|
1421
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1422
|
+
);
|
|
1361
1423
|
}
|
|
1362
1424
|
wait_for_processes_gone(&pids, std::time::Duration::from_secs(1));
|
|
1363
1425
|
}
|
|
1364
1426
|
|
|
1365
1427
|
fn reap_process_groups(pgids: &[u32], protected: &ShutdownProtection) {
|
|
1428
|
+
// 0.5.x Windows portability Batch 3: routes group termination
|
|
1429
|
+
// through `platform::process::terminate_group`. Unix keeps
|
|
1430
|
+
// `kill(-pgid, SIGTERM|SIGKILL)` semantics byte-for-byte;
|
|
1431
|
+
// Windows returns AlreadyGone (no pgid concept — Job Object
|
|
1432
|
+
// teardown is the shim-side concern per design §Route B).
|
|
1433
|
+
// The `pgid_t <= 1 || protected.contains_pgid(...)` filter
|
|
1434
|
+
// stays in place so pgid 0/1 (kernel/init) and the caller's
|
|
1435
|
+
// own group are never targeted.
|
|
1366
1436
|
for pgid in pgids {
|
|
1367
|
-
|
|
1368
|
-
continue;
|
|
1369
|
-
};
|
|
1370
|
-
if pgid_t <= 1 || protected.contains_pgid(*pgid) {
|
|
1437
|
+
if *pgid <= 1 || protected.contains_pgid(*pgid) {
|
|
1371
1438
|
continue;
|
|
1372
1439
|
}
|
|
1373
|
-
|
|
1440
|
+
let _ = crate::platform::process::terminate_group(
|
|
1441
|
+
*pgid,
|
|
1442
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
1443
|
+
);
|
|
1374
1444
|
}
|
|
1375
1445
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1376
1446
|
for pgid in pgids {
|
|
1377
|
-
|
|
1378
|
-
continue;
|
|
1379
|
-
};
|
|
1380
|
-
if pgid_t <= 1 || protected.contains_pgid(*pgid) {
|
|
1447
|
+
if *pgid <= 1 || protected.contains_pgid(*pgid) {
|
|
1381
1448
|
continue;
|
|
1382
1449
|
}
|
|
1383
|
-
|
|
1450
|
+
let _ = crate::platform::process::terminate_group(
|
|
1451
|
+
*pgid,
|
|
1452
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1453
|
+
);
|
|
1384
1454
|
}
|
|
1385
1455
|
}
|
|
1386
1456
|
|
|
@@ -1705,7 +1775,10 @@ pub mod lifecycle_port {
|
|
|
1705
1775
|
let mut protected = ShutdownProtection::default();
|
|
1706
1776
|
let current = std::process::id();
|
|
1707
1777
|
protected.pids.insert(current);
|
|
1708
|
-
|
|
1778
|
+
// 0.5.x Windows portability Batch 3: use platform primitive.
|
|
1779
|
+
// Windows returns None (no pgid concept) and the branch is
|
|
1780
|
+
// skipped honestly.
|
|
1781
|
+
if let Some(pgid) = crate::platform::process::current_process_group() {
|
|
1709
1782
|
protected.pgids.insert(pgid);
|
|
1710
1783
|
}
|
|
1711
1784
|
let mut cursor = current;
|
|
@@ -1840,56 +1913,33 @@ pub mod lifecycle_port {
|
|
|
1840
1913
|
}
|
|
1841
1914
|
}
|
|
1842
1915
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
fn send_process_signal_group(pgid: libc::pid_t, signal: libc::c_int) {
|
|
1853
|
-
unsafe {
|
|
1854
|
-
libc::kill(-pgid, signal);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1916
|
+
// 0.5.x Windows portability Batch 3: the four inline helpers
|
|
1917
|
+
// (send_process_signal, send_process_signal_group,
|
|
1918
|
+
// reap_child_if_possible, process_is_live) have been replaced
|
|
1919
|
+
// by direct calls through `crate::platform::process::*` at the
|
|
1920
|
+
// shutdown callsites above. The private helpers are removed so
|
|
1921
|
+
// there's a single source of truth for signal/waitpid/liveness
|
|
1922
|
+
// semantics; Unix behavior is byte-preserving (SignalKind maps
|
|
1923
|
+
// 1:1 to SIGTERM/SIGKILL, ProcessLiveness::Live == the previous
|
|
1924
|
+
// `kill(pid, 0) == 0 || EPERM` branch).
|
|
1857
1925
|
|
|
1858
1926
|
fn wait_for_processes_gone(pids: &[u32], timeout: std::time::Duration) {
|
|
1859
1927
|
let start = std::time::Instant::now();
|
|
1860
1928
|
loop {
|
|
1861
1929
|
for pid in pids {
|
|
1862
|
-
reap_child_if_possible(*pid);
|
|
1930
|
+
crate::platform::process::reap_child_if_possible(*pid);
|
|
1863
1931
|
}
|
|
1864
|
-
if !pids
|
|
1932
|
+
if !pids
|
|
1933
|
+
.iter()
|
|
1934
|
+
.any(|pid| crate::platform::process::pid_is_alive(*pid))
|
|
1935
|
+
|| start.elapsed() >= timeout
|
|
1936
|
+
{
|
|
1865
1937
|
return;
|
|
1866
1938
|
}
|
|
1867
1939
|
std::thread::sleep(std::time::Duration::from_millis(25));
|
|
1868
1940
|
}
|
|
1869
1941
|
}
|
|
1870
1942
|
|
|
1871
|
-
fn reap_child_if_possible(pid: u32) {
|
|
1872
|
-
let Ok(pid_t) = libc::pid_t::try_from(pid) else {
|
|
1873
|
-
return;
|
|
1874
|
-
};
|
|
1875
|
-
let mut status = 0;
|
|
1876
|
-
unsafe {
|
|
1877
|
-
libc::waitpid(pid_t, &mut status, libc::WNOHANG);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
fn process_is_live(pid: u32) -> bool {
|
|
1882
|
-
let Ok(pid_t) = libc::pid_t::try_from(pid) else {
|
|
1883
|
-
return false;
|
|
1884
|
-
};
|
|
1885
|
-
let rc = unsafe { libc::kill(pid_t, 0) };
|
|
1886
|
-
if rc == 0 {
|
|
1887
|
-
return true;
|
|
1888
|
-
}
|
|
1889
|
-
let err = std::io::Error::last_os_error();
|
|
1890
|
-
err.raw_os_error() == Some(libc::EPERM)
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
1943
|
fn process_pgids(
|
|
1894
1944
|
pids: &[u32],
|
|
1895
1945
|
protected: &ShutdownProtection,
|
|
@@ -1900,8 +1950,15 @@ pub mod lifecycle_port {
|
|
|
1900
1950
|
.filter_map(|pid| table.iter().find(|process| process.pid == *pid))
|
|
1901
1951
|
.filter_map(|process| process.pgid)
|
|
1902
1952
|
.filter(|pgid| {
|
|
1903
|
-
libc::pid_t
|
|
1904
|
-
|
|
1953
|
+
// 0.5.x Windows portability Batch 4: `libc::pid_t` was
|
|
1954
|
+
// used here as a signed-integer conversion gate to
|
|
1955
|
+
// reject values > INT_MAX. Replace with the equivalent
|
|
1956
|
+
// `i32::try_from` (pgid_t is `c_int` on every Unix we
|
|
1957
|
+
// support). Windows has no pgid concept so `pgids`
|
|
1958
|
+
// is empty in practice — the filter is dead code on
|
|
1959
|
+
// Windows but must still compile.
|
|
1960
|
+
i32::try_from(*pgid)
|
|
1961
|
+
.map(|pgid_int| pgid_int > 1 && !protected.contains_pgid(*pgid))
|
|
1905
1962
|
.unwrap_or(false)
|
|
1906
1963
|
})
|
|
1907
1964
|
.collect::<Vec<_>>();
|
|
@@ -1927,7 +1984,10 @@ pub mod lifecycle_port {
|
|
|
1927
1984
|
.map(|process| process.pid)
|
|
1928
1985
|
.collect::<std::collections::BTreeSet<_>>();
|
|
1929
1986
|
for pid in root_pids {
|
|
1930
|
-
if !protected.contains_pid(*pid)
|
|
1987
|
+
if !protected.contains_pid(*pid)
|
|
1988
|
+
&& crate::platform::process::pid_is_alive(*pid)
|
|
1989
|
+
&& seen.insert(*pid)
|
|
1990
|
+
{
|
|
1931
1991
|
residuals.push(ProcessInfo {
|
|
1932
1992
|
pid: *pid,
|
|
1933
1993
|
ppid: 0,
|
|
@@ -2873,14 +2933,38 @@ pub mod lifecycle_port {
|
|
|
2873
2933
|
);
|
|
2874
2934
|
}
|
|
2875
2935
|
}
|
|
2936
|
+
if let crate::provider::session::ResumeRefusalReason::SessionIdentityMismatch {
|
|
2937
|
+
expected_agent_id,
|
|
2938
|
+
embedded_agent_id,
|
|
2939
|
+
session_id,
|
|
2940
|
+
rollout_path,
|
|
2941
|
+
} = reason
|
|
2942
|
+
{
|
|
2943
|
+
entry.insert(
|
|
2944
|
+
"expected_agent_id".to_string(),
|
|
2945
|
+
json!(expected_agent_id),
|
|
2946
|
+
);
|
|
2947
|
+
entry.insert(
|
|
2948
|
+
"embedded_agent_id".to_string(),
|
|
2949
|
+
json!(embedded_agent_id),
|
|
2950
|
+
);
|
|
2951
|
+
entry.insert(
|
|
2952
|
+
"poisoned_session_id".to_string(),
|
|
2953
|
+
json!(session_id),
|
|
2954
|
+
);
|
|
2955
|
+
if let Some(path) = rollout_path {
|
|
2956
|
+
entry.insert(
|
|
2957
|
+
"rollout_path".to_string(),
|
|
2958
|
+
json!(path.to_string_lossy()),
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2876
2962
|
}
|
|
2877
2963
|
Value::Object(entry)
|
|
2878
2964
|
})
|
|
2879
2965
|
.collect();
|
|
2880
|
-
let unresumable_ids: Vec<&str> =
|
|
2881
|
-
.iter()
|
|
2882
|
-
.map(|w| w.agent_id.as_str())
|
|
2883
|
-
.collect();
|
|
2966
|
+
let unresumable_ids: Vec<&str> =
|
|
2967
|
+
unresumable.iter().map(|w| w.agent_id.as_str()).collect();
|
|
2884
2968
|
// Layer 2 self-healing (leader follow-up 2026-06-22): when
|
|
2885
2969
|
// EVERY unresumable worker shares the same structured
|
|
2886
2970
|
// refusal_reason wire string, refine the top-level `status`
|
|
@@ -2971,6 +3055,46 @@ pub mod lifecycle_port {
|
|
|
2971
3055
|
unresumable_ids.join(", "),
|
|
2972
3056
|
),
|
|
2973
3057
|
),
|
|
3058
|
+
"session_identity_mismatch" => {
|
|
3059
|
+
let mut lines = Vec::new();
|
|
3060
|
+
for w in unresumable.iter() {
|
|
3061
|
+
if let Some(
|
|
3062
|
+
crate::provider::session::ResumeRefusalReason::SessionIdentityMismatch {
|
|
3063
|
+
expected_agent_id,
|
|
3064
|
+
embedded_agent_id,
|
|
3065
|
+
session_id,
|
|
3066
|
+
rollout_path,
|
|
3067
|
+
},
|
|
3068
|
+
) = w.refusal_reason.as_ref()
|
|
3069
|
+
{
|
|
3070
|
+
let path = rollout_path
|
|
3071
|
+
.as_ref()
|
|
3072
|
+
.map(|p| p.to_string_lossy().into_owned())
|
|
3073
|
+
.unwrap_or_else(|| "<unknown>".to_string());
|
|
3074
|
+
lines.push(format!(
|
|
3075
|
+
" {}: session {} points to transcript identity {} at {} (expected {})",
|
|
3076
|
+
w.agent_id.as_str(),
|
|
3077
|
+
session_id,
|
|
3078
|
+
embedded_agent_id,
|
|
3079
|
+
path,
|
|
3080
|
+
expected_agent_id
|
|
3081
|
+
));
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
let mut msg = format!(
|
|
3085
|
+
"restart refused: provider session identity mismatch for {} worker(s) ({}). Pass --allow-fresh to discard the poisoned tuple and start fresh.",
|
|
3086
|
+
unresumable.len(),
|
|
3087
|
+
unresumable_ids.join(", "),
|
|
3088
|
+
);
|
|
3089
|
+
if !lines.is_empty() {
|
|
3090
|
+
msg.push_str("\nMismatched sessions:");
|
|
3091
|
+
for line in &lines {
|
|
3092
|
+
msg.push('\n');
|
|
3093
|
+
msg.push_str(line);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
("refused_session_identity_mismatch".to_string(), msg)
|
|
3097
|
+
}
|
|
2974
3098
|
_ => ("refused_resume_atomicity".to_string(), error.clone()),
|
|
2975
3099
|
}
|
|
2976
3100
|
} else {
|
|
@@ -355,6 +355,9 @@ fn resolve_leader_name_after_rebind() {
|
|
|
355
355
|
let _ = std::fs::remove_dir_all(&ws);
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// 0.5.x Windows portability Batch 5: `FakeAppServer` uses Unix
|
|
359
|
+
// domain sockets — Unix-only.
|
|
360
|
+
#[cfg(unix)]
|
|
358
361
|
#[test]
|
|
359
362
|
fn resolve_app_server_leader_name_uses_typed_receiver_not_tmux_pane() {
|
|
360
363
|
let ws = named_ws("appserver-leader-resolve");
|
|
@@ -419,6 +422,7 @@ fn resolve_app_server_leader_name_fails_closed_on_transport_conflict() {
|
|
|
419
422
|
let _ = std::fs::remove_dir_all(&ws);
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
#[cfg(unix)]
|
|
422
426
|
#[test]
|
|
423
427
|
fn send_to_name_app_server_leader_submits_turn_without_tmux_pane() {
|
|
424
428
|
let ws = named_ws("appserver-leader-send");
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
//! Minimal Codex app-server Unix-socket WebSocket client for leader delivery.
|
|
2
|
+
//!
|
|
3
|
+
//! 0.5.x Windows portability Batch 1: this client is fundamentally
|
|
4
|
+
//! Unix-only (`UnixStream` WebSocket handshake, uid/mode socket
|
|
5
|
+
//! ownership check). On Windows we still expose the same public API
|
|
6
|
+
//! surface so `messaging/delivery.rs`, `cli/send.rs`, `cli/named_address.rs`,
|
|
7
|
+
//! `leader/lease.rs` compile identically. Windows implementations of
|
|
8
|
+
//! `attach_probe` + `submit_to_bound_thread` return
|
|
9
|
+
//! `AppServerError::SocketUnreachable("codex_app_server unix socket not supported on this platform (Windows)")`
|
|
10
|
+
//! (N38 three-line typed unsupported: code+reason+action are baked into
|
|
11
|
+
//! `AppServerError::code()` and the `Display` impl). Truth source:
|
|
12
|
+
//! `.team/artifacts/0.5.x-windows-portability-survey-design.md` §Batch 1.
|
|
13
|
+
//!
|
|
14
|
+
//! Portable pieces below (types, JSON helpers) stay unconditional so
|
|
15
|
+
//! `receiver_is_app_server` / `binding_from_receiver` work identically
|
|
16
|
+
//! everywhere.
|
|
2
17
|
|
|
3
18
|
use serde_json::{json, Value};
|
|
19
|
+
#[cfg(unix)]
|
|
4
20
|
use std::io::{Read, Write};
|
|
21
|
+
#[cfg(unix)]
|
|
5
22
|
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
|
23
|
+
#[cfg(unix)]
|
|
6
24
|
use std::os::unix::net::UnixStream;
|
|
7
|
-
|
|
25
|
+
#[cfg(unix)]
|
|
26
|
+
use std::path::Path;
|
|
27
|
+
use std::path::PathBuf;
|
|
28
|
+
#[cfg(unix)]
|
|
8
29
|
use std::time::Duration;
|
|
9
30
|
|
|
10
31
|
pub const APP_SERVER_PROTOCOL_FIXTURE_CLI_MIN: &str = "0.139.0";
|
|
@@ -79,6 +100,7 @@ impl std::fmt::Display for AppServerError {
|
|
|
79
100
|
|
|
80
101
|
impl std::error::Error for AppServerError {}
|
|
81
102
|
|
|
103
|
+
#[cfg(unix)]
|
|
82
104
|
pub fn attach_probe(endpoint: &str, thread_id: &str) -> Result<AppServerBinding, AppServerError> {
|
|
83
105
|
check_socket_ownership(endpoint)?;
|
|
84
106
|
let socket = socket_path(endpoint)?;
|
|
@@ -95,6 +117,21 @@ pub fn attach_probe(endpoint: &str, thread_id: &str) -> Result<AppServerBinding,
|
|
|
95
117
|
})
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
/// Batch 1 Windows N38 typed-unsupported stub. Callers see the same
|
|
121
|
+
/// `AppServerError` shape as Unix — no silent success, no panic.
|
|
122
|
+
#[cfg(not(unix))]
|
|
123
|
+
pub fn attach_probe(
|
|
124
|
+
_endpoint: &str,
|
|
125
|
+
_thread_id: &str,
|
|
126
|
+
) -> Result<AppServerBinding, AppServerError> {
|
|
127
|
+
Err(AppServerError::SocketUnreachable(
|
|
128
|
+
"codex_app_server unix-socket client not supported on this platform \
|
|
129
|
+
(Windows); use codex CLI stdio or ConPTY worker delivery instead"
|
|
130
|
+
.to_string(),
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[cfg(unix)]
|
|
98
135
|
pub fn submit_to_bound_thread(
|
|
99
136
|
binding: &AppServerBinding,
|
|
100
137
|
message_id: &str,
|
|
@@ -108,6 +145,19 @@ pub fn submit_to_bound_thread(
|
|
|
108
145
|
client.turn_start(&binding.thread_id, message_id, rendered)
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
#[cfg(not(unix))]
|
|
149
|
+
pub fn submit_to_bound_thread(
|
|
150
|
+
_binding: &AppServerBinding,
|
|
151
|
+
_message_id: &str,
|
|
152
|
+
_rendered: &str,
|
|
153
|
+
) -> Result<AppServerSubmit, AppServerError> {
|
|
154
|
+
Err(AppServerError::SocketUnreachable(
|
|
155
|
+
"codex_app_server unix-socket submit not supported on this platform \
|
|
156
|
+
(Windows); use codex CLI stdio or ConPTY worker delivery instead"
|
|
157
|
+
.to_string(),
|
|
158
|
+
))
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
pub fn binding_from_receiver(receiver: &Value) -> Result<AppServerBinding, AppServerError> {
|
|
112
162
|
let app = receiver
|
|
113
163
|
.get("app_server")
|
|
@@ -135,6 +185,7 @@ fn required_str<'a>(value: &'a Value, field: &str) -> Result<&'a str, AppServerE
|
|
|
135
185
|
.ok_or_else(|| AppServerError::ProtocolMismatch(format!("missing {field}")))
|
|
136
186
|
}
|
|
137
187
|
|
|
188
|
+
#[cfg(unix)]
|
|
138
189
|
fn validate_tuple(
|
|
139
190
|
binding: &AppServerBinding,
|
|
140
191
|
user_agent: &str,
|
|
@@ -163,6 +214,7 @@ fn validate_tuple(
|
|
|
163
214
|
Ok(())
|
|
164
215
|
}
|
|
165
216
|
|
|
217
|
+
#[cfg(unix)]
|
|
166
218
|
fn check_socket_ownership(endpoint: &str) -> Result<(), AppServerError> {
|
|
167
219
|
let path = socket_path(endpoint)?;
|
|
168
220
|
let meta = std::fs::metadata(&path)
|
|
@@ -189,6 +241,7 @@ fn check_socket_ownership(endpoint: &str) -> Result<(), AppServerError> {
|
|
|
189
241
|
Ok(())
|
|
190
242
|
}
|
|
191
243
|
|
|
244
|
+
#[cfg(unix)]
|
|
192
245
|
fn socket_path(endpoint: &str) -> Result<PathBuf, AppServerError> {
|
|
193
246
|
let raw = endpoint.strip_prefix("unix://").unwrap_or(endpoint);
|
|
194
247
|
if raw.is_empty() {
|
|
@@ -199,17 +252,20 @@ fn socket_path(endpoint: &str) -> Result<PathBuf, AppServerError> {
|
|
|
199
252
|
Ok(PathBuf::from(raw))
|
|
200
253
|
}
|
|
201
254
|
|
|
255
|
+
#[cfg(unix)]
|
|
202
256
|
struct ThreadResume {
|
|
203
257
|
thread_id: String,
|
|
204
258
|
session_id: String,
|
|
205
259
|
cwd: String,
|
|
206
260
|
}
|
|
207
261
|
|
|
262
|
+
#[cfg(unix)]
|
|
208
263
|
struct AppServerClient {
|
|
209
264
|
stream: UnixStream,
|
|
210
265
|
next_id: u64,
|
|
211
266
|
}
|
|
212
267
|
|
|
268
|
+
#[cfg(unix)]
|
|
213
269
|
impl AppServerClient {
|
|
214
270
|
fn connect(path: &Path) -> Result<Self, AppServerError> {
|
|
215
271
|
let mut stream = UnixStream::connect(path)
|
|
@@ -386,17 +442,20 @@ impl AppServerClient {
|
|
|
386
442
|
}
|
|
387
443
|
}
|
|
388
444
|
|
|
445
|
+
#[cfg(unix)]
|
|
389
446
|
fn is_approval_method(method: &str) -> bool {
|
|
390
447
|
method.contains("requestApproval")
|
|
391
448
|
|| method.contains("requestUserInput")
|
|
392
449
|
|| method.contains("elicitation/request")
|
|
393
450
|
}
|
|
394
451
|
|
|
452
|
+
#[cfg(unix)]
|
|
395
453
|
fn looks_busy(message: &str) -> bool {
|
|
396
454
|
let lower = message.to_ascii_lowercase();
|
|
397
455
|
lower.contains("active turn") || lower.contains("already has") || lower.contains("busy")
|
|
398
456
|
}
|
|
399
457
|
|
|
458
|
+
#[cfg(unix)]
|
|
400
459
|
fn read_http_response(stream: &mut UnixStream) -> Result<String, AppServerError> {
|
|
401
460
|
let mut data = Vec::new();
|
|
402
461
|
let mut buf = [0u8; 1];
|
|
@@ -414,6 +473,7 @@ fn read_http_response(stream: &mut UnixStream) -> Result<String, AppServerError>
|
|
|
414
473
|
Ok(String::from_utf8_lossy(&data).to_string())
|
|
415
474
|
}
|
|
416
475
|
|
|
476
|
+
#[cfg(unix)]
|
|
417
477
|
fn write_ws_text(stream: &mut UnixStream, text: &str) -> Result<(), AppServerError> {
|
|
418
478
|
let payload = text.as_bytes();
|
|
419
479
|
let mut frame = vec![0x81];
|
|
@@ -436,6 +496,7 @@ fn write_ws_text(stream: &mut UnixStream, text: &str) -> Result<(), AppServerErr
|
|
|
436
496
|
.map_err(|e| AppServerError::Io(e.to_string()))
|
|
437
497
|
}
|
|
438
498
|
|
|
499
|
+
#[cfg(unix)]
|
|
439
500
|
fn read_ws_text(stream: &mut UnixStream) -> Result<String, AppServerError> {
|
|
440
501
|
loop {
|
|
441
502
|
let mut header = [0u8; 2];
|