@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
|
@@ -53,6 +53,21 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
|
|
|
53
53
|
/// `start_coordinator`(`lifecycle.py:49-121`):幂等 — 已健康 no-op(AlreadyRunning);metadata 不兼容
|
|
54
54
|
/// 先 stop 再起;schema 不兼容拒启 + hint;否则 spawn `team-agent coordinator --workspace <ws>`。
|
|
55
55
|
pub fn start_coordinator(workspace: &WorkspacePath) -> Result<StartReport, StartError> {
|
|
56
|
+
start_coordinator_with_team(workspace, None)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
60
|
+
/// forward `--team` to the spawned coord daemon so it doesn't have
|
|
61
|
+
/// to derive team_key from `state.active_team_key` at boot. The
|
|
62
|
+
/// derivation stays as fallback (see `coordinator::backoff::run_daemon`),
|
|
63
|
+
/// so Unix daemons and pre-existing test harnesses are byte-preserving.
|
|
64
|
+
///
|
|
65
|
+
/// Callers that CAN pass team_key (Batch 9 quick-start Windows path)
|
|
66
|
+
/// SHOULD — that avoids Batch 8's F8 seed-state trap.
|
|
67
|
+
pub fn start_coordinator_with_team(
|
|
68
|
+
workspace: &WorkspacePath,
|
|
69
|
+
team_key: Option<&str>,
|
|
70
|
+
) -> Result<StartReport, StartError> {
|
|
56
71
|
let health = coordinator_health(workspace);
|
|
57
72
|
if health.ok {
|
|
58
73
|
return Ok(StartReport {
|
|
@@ -101,7 +116,13 @@ pub fn start_coordinator(workspace: &WorkspacePath) -> Result<StartReport, Start
|
|
|
101
116
|
let mut command = Command::new(std::env::current_exe()?);
|
|
102
117
|
command
|
|
103
118
|
.args(["coordinator", "--workspace"])
|
|
104
|
-
.arg(workspace.as_path())
|
|
119
|
+
.arg(workspace.as_path());
|
|
120
|
+
if let Some(tk) = team_key {
|
|
121
|
+
if !tk.is_empty() {
|
|
122
|
+
command.args(["--team", tk]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
command
|
|
105
126
|
.stdin(Stdio::null())
|
|
106
127
|
.stdout(Stdio::from(log))
|
|
107
128
|
.stderr(Stdio::from(log_err));
|
|
@@ -135,7 +156,23 @@ fn detach_daemon_child(command: &mut Command) {
|
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
#[cfg(
|
|
159
|
+
#[cfg(windows)]
|
|
160
|
+
fn detach_daemon_child(command: &mut Command) {
|
|
161
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
162
|
+
// detach the coordinator daemon on Windows via
|
|
163
|
+
// `DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB` creation flags,
|
|
164
|
+
// matching what `coordinator::conpty_shim::spawn_shim_and_handshake`
|
|
165
|
+
// does for the shim. Without these flags, an SSH-launched
|
|
166
|
+
// quick-start blocks waiting for the coord daemon's process
|
|
167
|
+
// tree to exit (never happens — daemon runs forever), and the
|
|
168
|
+
// quick-start caller sees a hung command.
|
|
169
|
+
use std::os::windows::process::CommandExt;
|
|
170
|
+
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
171
|
+
const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x01000000;
|
|
172
|
+
command.creation_flags(DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[cfg(not(any(unix, windows)))]
|
|
139
176
|
fn detach_daemon_child(_command: &mut Command) {}
|
|
140
177
|
|
|
141
178
|
/// `stop_coordinator`(`lifecycle.py:228-247`):SIGTERM pid + 清 pid/meta → typed report。
|
|
@@ -272,16 +309,32 @@ fn coordinator_command_matches_workspace(command: &str, workspaces: &[String]) -
|
|
|
272
309
|
}
|
|
273
310
|
|
|
274
311
|
fn terminate_pid(pid: Pid) -> bool {
|
|
312
|
+
// 0.5.x Windows portability Batch 3: routes signal delivery through
|
|
313
|
+
// `platform::process::terminate_pid`. Unix keeps
|
|
314
|
+
// SIGTERM → 5s grace → SIGKILL semantics byte-for-byte
|
|
315
|
+
// (`SignalKind::TerminateGraceful` → SIGTERM,
|
|
316
|
+
// `SignalKind::TerminateForce` → SIGKILL). Windows performs
|
|
317
|
+
// `TerminateProcess` for both kinds; the `TerminationOutcome::ForceOnly`
|
|
318
|
+
// return on the graceful call is what a future audit-event
|
|
319
|
+
// emitter (CR C-6) will trigger `platform.terminate_force_only`
|
|
320
|
+
// on. For this batch the return value is discarded, matching the
|
|
321
|
+
// current inline `let _ = send_signal(...)` pattern.
|
|
275
322
|
if pid_is_running(pid).ok() == Some(false) {
|
|
276
323
|
return true;
|
|
277
324
|
}
|
|
278
325
|
let pids = process_tree_pids(pid);
|
|
279
326
|
for child in pids.iter().rev() {
|
|
280
|
-
let _ =
|
|
327
|
+
let _ = crate::platform::process::terminate_pid(
|
|
328
|
+
child.get(),
|
|
329
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
330
|
+
);
|
|
281
331
|
}
|
|
282
332
|
if !wait_until_all_not_running(&pids, Duration::from_secs(5)) {
|
|
283
333
|
for child in pids.iter().rev() {
|
|
284
|
-
let _ =
|
|
334
|
+
let _ = crate::platform::process::terminate_pid(
|
|
335
|
+
child.get(),
|
|
336
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
337
|
+
);
|
|
285
338
|
}
|
|
286
339
|
}
|
|
287
340
|
wait_until_all_not_running(&pids, Duration::from_secs(5))
|
|
@@ -348,16 +401,17 @@ fn wait_until_all_not_running(pids: &[Pid], timeout: Duration) -> bool {
|
|
|
348
401
|
}
|
|
349
402
|
|
|
350
403
|
fn reap_child_if_possible(pid: Pid) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let mut status = 0;
|
|
355
|
-
unsafe {
|
|
356
|
-
libc::waitpid(pid_t, &mut status, libc::WNOHANG);
|
|
357
|
-
}
|
|
404
|
+
// Batch 3: routed through `platform::process`. Unix `waitpid
|
|
405
|
+
// (WNOHANG)`; Windows no-op (no zombie model).
|
|
406
|
+
crate::platform::process::reap_child_if_possible(pid.get());
|
|
358
407
|
}
|
|
359
408
|
|
|
409
|
+
#[cfg(unix)]
|
|
410
|
+
#[allow(dead_code)]
|
|
360
411
|
fn send_signal(pid: Pid, signal: libc::c_int) -> bool {
|
|
412
|
+
// Retained (dead code post-Batch-3) as a Unix-only helper for any
|
|
413
|
+
// future non-standard signal delivery. All product paths now use
|
|
414
|
+
// `crate::platform::process::terminate_pid` with `SignalKind`.
|
|
361
415
|
let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
|
|
362
416
|
return false;
|
|
363
417
|
};
|
|
@@ -383,6 +437,21 @@ fn wait_until_not_running(pid: Pid, timeout: Duration) -> bool {
|
|
|
383
437
|
|
|
384
438
|
/// `pid_is_running`(`metadata.py:16-25`):`os.kill(pid, 0)` + `ps -o stat=` 查 zombie(Z* → 不算活)。
|
|
385
439
|
/// §10 fallible:进程探测 I/O 可失败 → Result。
|
|
440
|
+
///
|
|
441
|
+
/// 0.5.x Windows portability Batch 4: this function has UNIQUE
|
|
442
|
+
/// coordinator-metadata semantics — it treats `EPERM` as "not
|
|
443
|
+
/// running" (different from `platform::process::pid_liveness` which
|
|
444
|
+
/// treats `EPERM` as Live) because the coordinator only owns
|
|
445
|
+
/// processes it can signal, and uses `ps -o stat=` for zombie
|
|
446
|
+
/// detection. The `ps` shellout is Unix-only.
|
|
447
|
+
///
|
|
448
|
+
/// On Windows the coordinator-metadata identity check runs through
|
|
449
|
+
/// `platform::process::pid_liveness` instead (Windows has no zombie
|
|
450
|
+
/// state — a process is either Live or Dead — so the additional
|
|
451
|
+
/// `ps stat=` step has no analogue). This preserves the coordinator's
|
|
452
|
+
/// "am I the owner" check without silently reporting stale pids as
|
|
453
|
+
/// alive.
|
|
454
|
+
#[cfg(unix)]
|
|
386
455
|
pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
387
456
|
let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
|
|
388
457
|
return Ok(false);
|
|
@@ -407,6 +476,23 @@ pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
|
407
476
|
Ok(!stat.is_empty() && !stat.starts_with('Z'))
|
|
408
477
|
}
|
|
409
478
|
|
|
479
|
+
/// Windows shim: no `ps stat=` zombie detection needed (Windows has
|
|
480
|
+
/// no zombie state — a process is either Live or Dead). Route through
|
|
481
|
+
/// `platform::process::pid_liveness` and map to bool. The EPERM
|
|
482
|
+
/// semantic ("we can't signal → treat as not running") maps to
|
|
483
|
+
/// Windows `ERROR_ACCESS_DENIED` which the platform layer already
|
|
484
|
+
/// treats as `Live`; so on Windows the coordinator sees a process
|
|
485
|
+
/// it can't query as still-running (safer than pretending it's gone
|
|
486
|
+
/// and losing the ownership handle).
|
|
487
|
+
#[cfg(not(unix))]
|
|
488
|
+
pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
489
|
+
match crate::platform::process::pid_liveness(pid.get())? {
|
|
490
|
+
crate::platform::process::ProcessLiveness::Live => Ok(true),
|
|
491
|
+
crate::platform::process::ProcessLiveness::Dead => Ok(false),
|
|
492
|
+
crate::platform::process::ProcessLiveness::Unknown { .. } => Ok(false),
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
410
496
|
/// `read_coordinator_metadata`(`metadata.py:28-34`)。读 `coordinator.json`;损坏/缺失/非 dict → `None`。
|
|
411
497
|
pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<CoordinatorMetadata> {
|
|
412
498
|
let text = std::fs::read_to_string(coordinator_meta_path(workspace)).ok()?;
|
|
@@ -62,6 +62,14 @@ use crate::provider::{ProviderAdapter, TurnId, TurnState};
|
|
|
62
62
|
use serde_json::Value;
|
|
63
63
|
|
|
64
64
|
pub mod backoff;
|
|
65
|
+
// 0.5.x Windows portability Batch 6 Option A: shim lifecycle
|
|
66
|
+
// manager. Windows-only per design §Shim Lifecycle (the shim binary
|
|
67
|
+
// exists only on Windows; Unix uses tmux). Cfg-gated at the module
|
|
68
|
+
// level so callers can `crate::coordinator::conpty_shim::...`
|
|
69
|
+
// uniformly on Windows and get a compile error (rather than a
|
|
70
|
+
// silent no-op) on Unix — Unix code paths never reach for the shim.
|
|
71
|
+
#[cfg(windows)]
|
|
72
|
+
pub mod conpty_shim;
|
|
65
73
|
pub mod health;
|
|
66
74
|
pub mod orphan;
|
|
67
75
|
pub mod runtime_detectors;
|
|
@@ -87,7 +87,7 @@ fn start_coordinator_fresh_workspace_decides_started() {
|
|
|
87
87
|
fn run_daemon_once_writes_boot_metadata_and_returns_ok() {
|
|
88
88
|
let (wp, dir) = daemon_ws();
|
|
89
89
|
crate::state::persist::save_runtime_state(&dir, &serde_json::json!({"session_name": "team-x", "agents": {}})).unwrap();
|
|
90
|
-
let r = run_daemon(DaemonArgs { workspace: wp.clone(), once: true, tick_interval_sec: None });
|
|
90
|
+
let r = run_daemon(DaemonArgs { workspace: wp.clone(), once: true, tick_interval_sec: None, team_key: None });
|
|
91
91
|
assert!(r.is_ok(), "run_daemon --once must return Ok; got {r:?}");
|
|
92
92
|
assert!(coordinator_meta_path(&wp).exists(), "run_daemon must write the coordinator boot metadata");
|
|
93
93
|
}
|
|
@@ -438,6 +438,7 @@ fn run_daemon_backs_off_on_transient_tick_err_then_recovers_without_exiting() {
|
|
|
438
438
|
workspace: WorkspacePath::new(dir.clone()),
|
|
439
439
|
once: false,
|
|
440
440
|
tick_interval_sec: Some(0.01), // tiny backoff so the test is fast
|
|
441
|
+
team_key: None,
|
|
441
442
|
};
|
|
442
443
|
let result = run_daemon_with_coordinator(&args, &coord);
|
|
443
444
|
assert!(
|
|
@@ -192,6 +192,12 @@ fn p2_tick_skips_tmux_gate_when_session_name_absent() {
|
|
|
192
192
|
|
|
193
193
|
// P1 — pid_is_running must use os.kill(pid,0) first: a pid owned by another user (pid 1 /
|
|
194
194
|
// launchd, root) is EPERM → not signalable → False. Current only `ps -p` (rc=0) → True.
|
|
195
|
+
//
|
|
196
|
+
// 0.5.x Windows portability Batch 5: this test's PREMISE is Unix
|
|
197
|
+
// specific (pid 1 = init/launchd, EPERM semantics). On Windows,
|
|
198
|
+
// pid_is_running routes through `platform::process::pid_liveness` which
|
|
199
|
+
// has different accessible-pid semantics. Unix-only test.
|
|
200
|
+
#[cfg(unix)]
|
|
195
201
|
#[test]
|
|
196
202
|
fn p2_pid_is_running_false_for_cross_user_pid() {
|
|
197
203
|
// The cross-user semantics only hold for a non-root caller (root CAN signal pid 1).
|
|
@@ -10,7 +10,8 @@ use crate::coordinator::health::{
|
|
|
10
10
|
coordinator_metadata_ok, pid_is_running, read_coordinator_metadata, terminate_pid_tree,
|
|
11
11
|
};
|
|
12
12
|
use crate::coordinator::types::{OrphanReason, Pid, WorkspacePath};
|
|
13
|
-
|
|
13
|
+
// Phase 1d Batch 6: `TmuxBackend` import removed — all sites now use
|
|
14
|
+
// `transport_factory::tmux_socket_name_transport` for grep-visibility.
|
|
14
15
|
use crate::transport::{SessionName, Transport};
|
|
15
16
|
|
|
16
17
|
#[derive(Debug, Clone)]
|
|
@@ -341,18 +342,33 @@ fn tmux_socket_names() -> Vec<String> {
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
fn tmux_socket_roots() -> Vec<PathBuf> {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
345
|
+
// 0.5.x Windows portability Batch 1: tmux orphan scan is Unix-only
|
|
346
|
+
// (design §Layering Strategy — "ConPTY orphan diagnostics should be
|
|
347
|
+
// a separate shim registry command later"). On Windows return
|
|
348
|
+
// empty so the caller loops zero times honestly.
|
|
349
|
+
#[cfg(unix)]
|
|
350
|
+
{
|
|
351
|
+
let uid = unsafe { libc::geteuid() };
|
|
352
|
+
let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
|
|
353
|
+
if let Some(tmpdir) = std::env::var_os("TMPDIR") {
|
|
354
|
+
roots.push(PathBuf::from(tmpdir).join(format!("tmux-{uid}")));
|
|
355
|
+
}
|
|
356
|
+
roots.sort();
|
|
357
|
+
roots.dedup();
|
|
358
|
+
roots
|
|
359
|
+
}
|
|
360
|
+
#[cfg(not(unix))]
|
|
361
|
+
{
|
|
362
|
+
Vec::new()
|
|
348
363
|
}
|
|
349
|
-
roots.sort();
|
|
350
|
-
roots.dedup();
|
|
351
|
-
roots
|
|
352
364
|
}
|
|
353
365
|
|
|
354
366
|
fn tmux_list_panes(socket: &str) -> Vec<TmuxPaneRow> {
|
|
355
|
-
|
|
367
|
+
// Phase 1d Batch 6: factory tmux-socket-name helper for
|
|
368
|
+
// grep-visibility. Tmux-only orphan scanner (enumerates tmux socket
|
|
369
|
+
// roots); ConPTY orphan diagnostics are a separate shim registry
|
|
370
|
+
// command, not routed here.
|
|
371
|
+
crate::transport_factory::tmux_socket_name_transport(socket)
|
|
356
372
|
.list_targets()
|
|
357
373
|
.unwrap_or_default()
|
|
358
374
|
.into_iter()
|
|
@@ -664,7 +680,10 @@ fn kill_tmux_session(orphan: &OrphanRecord) -> bool {
|
|
|
664
680
|
let (Some(socket), Some(session)) = (&orphan.tmux_socket, &orphan.session) else {
|
|
665
681
|
return false;
|
|
666
682
|
};
|
|
667
|
-
|
|
683
|
+
// Phase 1d Batch 6: factory tmux-socket-name helper. Explicit
|
|
684
|
+
// tmux-only kill; conpty shim orphans are NOT routed here (design
|
|
685
|
+
// §Batch 6).
|
|
686
|
+
crate::transport_factory::tmux_socket_name_transport(socket)
|
|
668
687
|
.kill_session(&SessionName::new(session.clone()))
|
|
669
688
|
.is_ok()
|
|
670
689
|
}
|
|
@@ -134,6 +134,58 @@ pub fn attach_leader(
|
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/// Explicit app-server leader binding. Validates the supplied socket/thread tuple
|
|
138
|
+
/// before writing the typed physical-channel anchor through the lease primitive.
|
|
139
|
+
pub fn attach_app_server_leader(
|
|
140
|
+
workspace: &Path,
|
|
141
|
+
team: Option<&str>,
|
|
142
|
+
socket: &str,
|
|
143
|
+
thread_id: &str,
|
|
144
|
+
) -> Result<Value, LeaderError> {
|
|
145
|
+
let binding = crate::codex_app_server::attach_probe(socket, thread_id)
|
|
146
|
+
.map_err(|error| LeaderError::Validation(error.to_string()))?;
|
|
147
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
148
|
+
let scoped_team = team.filter(|value| !value.is_empty());
|
|
149
|
+
let mut state = if scoped_team.is_some() {
|
|
150
|
+
crate::state::projection::select_runtime_state(workspace, scoped_team)?
|
|
151
|
+
} else {
|
|
152
|
+
crate::state::persist::load_runtime_state(workspace)?
|
|
153
|
+
};
|
|
154
|
+
if !state.is_object() {
|
|
155
|
+
state = json!({});
|
|
156
|
+
}
|
|
157
|
+
if let Some(team) = scoped_team {
|
|
158
|
+
state["active_team_key"] = json!(team);
|
|
159
|
+
}
|
|
160
|
+
let team_key = canonical_owner_write_key(&state);
|
|
161
|
+
let next_epoch = OwnerEpoch(current_owner_epoch(&state).0.saturating_add(1));
|
|
162
|
+
let receiver = app_server_receiver_value(&binding, next_epoch);
|
|
163
|
+
let owner = app_server_owner_value(next_epoch);
|
|
164
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
165
|
+
.with_leader_receiver(receiver.clone())
|
|
166
|
+
.with_team_owner(owner.clone())
|
|
167
|
+
.with_owner_epoch(next_epoch.0);
|
|
168
|
+
crate::state::ownership::write_owner(&mut state, &team_key, record);
|
|
169
|
+
write_claim_state(workspace, &state, scoped_team, Some(&team_key))?;
|
|
170
|
+
event_log.write(
|
|
171
|
+
super::LeaderEvent::ReceiverAttached.name(),
|
|
172
|
+
json!({
|
|
173
|
+
"transport_kind": "codex_app_server",
|
|
174
|
+
"thread_id": binding.thread_id,
|
|
175
|
+
"owner_epoch": next_epoch.0,
|
|
176
|
+
"team": team_key,
|
|
177
|
+
}),
|
|
178
|
+
)?;
|
|
179
|
+
Ok(json!({
|
|
180
|
+
"ok": true,
|
|
181
|
+
"status": "claimed",
|
|
182
|
+
"team": team_key,
|
|
183
|
+
"owner_epoch": next_epoch.0,
|
|
184
|
+
"leader_receiver": receiver,
|
|
185
|
+
"team_owner": owner,
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
|
|
137
189
|
#[derive(Clone)]
|
|
138
190
|
struct AttachLeaderTarget {
|
|
139
191
|
info: PaneInfo,
|
|
@@ -156,8 +208,11 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
|
|
|
156
208
|
}),
|
|
157
209
|
);
|
|
158
210
|
}
|
|
211
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
212
|
+
// grep-visibility. Semantics unchanged; leader lease discovery is
|
|
213
|
+
// tmux-only (caller pane = tmux pane, MUST-12 anchor).
|
|
159
214
|
targets.extend(
|
|
160
|
-
crate::
|
|
215
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
161
216
|
.list_targets()
|
|
162
217
|
.unwrap_or_default()
|
|
163
218
|
.into_iter()
|
|
@@ -167,10 +222,12 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
|
|
|
167
222
|
}
|
|
168
223
|
|
|
169
224
|
fn tmux_backend_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
|
|
225
|
+
// Phase 1d Batch 6: factory tmux channel helpers for grep-visibility.
|
|
226
|
+
// Same tmux-only semantics as before.
|
|
170
227
|
if endpoint.is_empty() || endpoint == "default" {
|
|
171
|
-
crate::
|
|
228
|
+
crate::transport_factory::tmux_default_transport()
|
|
172
229
|
} else {
|
|
173
|
-
crate::
|
|
230
|
+
crate::transport_factory::tmux_endpoint_transport(endpoint)
|
|
174
231
|
}
|
|
175
232
|
}
|
|
176
233
|
|
|
@@ -320,11 +377,13 @@ pub fn claim_leader(
|
|
|
320
377
|
.unwrap_or_default();
|
|
321
378
|
let raw_state = crate::state::persist::load_runtime_state(workspace)?;
|
|
322
379
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
323
|
-
|
|
380
|
+
// Phase 1d Batch 6: factory tmux channel helpers for grep-visibility.
|
|
381
|
+
// Tmux-only leader-claim path (caller pane enumeration).
|
|
382
|
+
let mut targets = crate::transport_factory::tmux_workspace_transport(workspace)
|
|
324
383
|
.list_targets()
|
|
325
384
|
.unwrap_or_default();
|
|
326
385
|
targets.extend(
|
|
327
|
-
crate::
|
|
386
|
+
crate::transport_factory::tmux_default_transport()
|
|
328
387
|
.list_targets()
|
|
329
388
|
.unwrap_or_default(),
|
|
330
389
|
);
|
|
@@ -1055,7 +1114,7 @@ fn write_binding_to_state(
|
|
|
1055
1114
|
}
|
|
1056
1115
|
let team_key = canonical_owner_write_key(state);
|
|
1057
1116
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1058
|
-
.with_leader_receiver(
|
|
1117
|
+
.with_leader_receiver(tmux_receiver_value(receiver)?)
|
|
1059
1118
|
.with_team_owner(serde_json::to_value(owner)?)
|
|
1060
1119
|
.with_owner_epoch(owner.owner_epoch.0);
|
|
1061
1120
|
crate::state::ownership::write_owner(state, &team_key, record);
|
|
@@ -1074,11 +1133,89 @@ fn write_receiver_to_state(
|
|
|
1074
1133
|
}
|
|
1075
1134
|
let team_key = canonical_owner_write_key(state);
|
|
1076
1135
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1077
|
-
.with_leader_receiver(
|
|
1136
|
+
.with_leader_receiver(tmux_receiver_value(receiver)?);
|
|
1078
1137
|
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1079
1138
|
Ok(())
|
|
1080
1139
|
}
|
|
1081
1140
|
|
|
1141
|
+
fn tmux_receiver_value(receiver: &LeaderReceiver) -> Result<Value, LeaderError> {
|
|
1142
|
+
Ok(write_leader_receiver_transport(
|
|
1143
|
+
serde_json::to_value(receiver)?,
|
|
1144
|
+
"direct_tmux",
|
|
1145
|
+
))
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
fn app_server_receiver_value(
|
|
1149
|
+
binding: &crate::codex_app_server::AppServerBinding,
|
|
1150
|
+
epoch: OwnerEpoch,
|
|
1151
|
+
) -> Value {
|
|
1152
|
+
write_leader_receiver_transport(
|
|
1153
|
+
json!({
|
|
1154
|
+
"status": "attached",
|
|
1155
|
+
"provider": "codex",
|
|
1156
|
+
"owner_epoch": epoch.0,
|
|
1157
|
+
"app_server": {
|
|
1158
|
+
"socket": binding.socket,
|
|
1159
|
+
"thread_id": binding.thread_id,
|
|
1160
|
+
"session_id": binding.session_id,
|
|
1161
|
+
"cwd": binding.cwd,
|
|
1162
|
+
"cli_version": binding.cli_version,
|
|
1163
|
+
"bound_at": binding.bound_at,
|
|
1164
|
+
"source": "app-server"
|
|
1165
|
+
}
|
|
1166
|
+
}),
|
|
1167
|
+
"codex_app_server",
|
|
1168
|
+
)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
fn app_server_owner_value(epoch: OwnerEpoch) -> Value {
|
|
1172
|
+
json!({
|
|
1173
|
+
"provider": "codex",
|
|
1174
|
+
"transport_kind": "codex_app_server",
|
|
1175
|
+
"owner_epoch": epoch.0,
|
|
1176
|
+
"claimed_at": now_ts(),
|
|
1177
|
+
"claimed_via": "attach-app-server-leader",
|
|
1178
|
+
"os_user": std::env::var("USER")
|
|
1179
|
+
.or_else(|_| std::env::var("USERNAME"))
|
|
1180
|
+
.unwrap_or_default(),
|
|
1181
|
+
})
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
fn write_leader_receiver_transport(mut receiver: Value, transport_kind: &str) -> Value {
|
|
1185
|
+
if let Some(obj) = receiver.as_object_mut() {
|
|
1186
|
+
obj.insert("mode".to_string(), json!(transport_kind));
|
|
1187
|
+
match transport_kind {
|
|
1188
|
+
"codex_app_server" => {
|
|
1189
|
+
obj.insert("transport_kind".to_string(), json!(transport_kind));
|
|
1190
|
+
for legacy in [
|
|
1191
|
+
"pane_id",
|
|
1192
|
+
"tmux_socket",
|
|
1193
|
+
"session_name",
|
|
1194
|
+
"window_index",
|
|
1195
|
+
"window_name",
|
|
1196
|
+
"pane_index",
|
|
1197
|
+
"pane_tty",
|
|
1198
|
+
"pane_current_command",
|
|
1199
|
+
"fingerprint",
|
|
1200
|
+
"leader_session_uuid",
|
|
1201
|
+
"attached_at",
|
|
1202
|
+
"discovery",
|
|
1203
|
+
"requested_provider",
|
|
1204
|
+
"warning",
|
|
1205
|
+
] {
|
|
1206
|
+
obj.remove(legacy);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
"direct_tmux" => {
|
|
1210
|
+
obj.remove("app_server");
|
|
1211
|
+
obj.remove("transport_kind");
|
|
1212
|
+
}
|
|
1213
|
+
_ => {}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
receiver
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1082
1219
|
/// Stage 3 owner persist fix (architect direction 2026-06-24): determine
|
|
1083
1220
|
/// the canonical team_key for an in-memory ownership write. When the state
|
|
1084
1221
|
/// carries an `active_team_key` (set by `project_top_level_view` whenever
|
|
@@ -262,7 +262,9 @@ fn family_a_identity(
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
fn tmux_pane_current_command(workspace: &Path, pane: &str) -> Result<String, LeaderError> {
|
|
265
|
-
|
|
265
|
+
// Phase 1d Batch 6: factory tmux workspace helper for grep-visibility.
|
|
266
|
+
// Tmux-only owner-bind (caller pane = tmux pane).
|
|
267
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
266
268
|
.query(
|
|
267
269
|
&Target::Pane(PaneId::new(pane)),
|
|
268
270
|
PaneField::PaneCurrentCommand,
|
|
@@ -273,7 +275,8 @@ fn tmux_pane_current_command(workspace: &Path, pane: &str) -> Result<String, Lea
|
|
|
273
275
|
|
|
274
276
|
fn tmux_pane_info(workspace: &Path, pane: &str) -> Option<PaneInfo> {
|
|
275
277
|
let target = PaneId::new(pane);
|
|
276
|
-
|
|
278
|
+
// Phase 1d Batch 6: same as above.
|
|
279
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
277
280
|
.list_targets()
|
|
278
281
|
.ok()?
|
|
279
282
|
.into_iter()
|
|
@@ -98,44 +98,29 @@ fn provider_wire_for_command_attribution(provider: Provider) -> &'static str {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// 0.5.x Windows portability Batch 4: process command-line + environ
|
|
102
|
+
// probes route through `crate::platform::argv`. Unix keeps
|
|
103
|
+
// byte-preserving semantics (Linux `/proc/<pid>/cmdline` + `environ`;
|
|
104
|
+
// other Unix `ps -p ... -o command=`; macOS environ is `None` because
|
|
105
|
+
// the pre-batch code also returned `None` on non-Linux). Windows
|
|
106
|
+
// returns `None` per design §Batch 4 conservative anchor (unknown
|
|
107
|
+
// argv must never infer elevated approval).
|
|
108
|
+
//
|
|
109
|
+
// The command-line format expected by the caller is a
|
|
110
|
+
// whitespace-joined single string, so we join the argv tokens after
|
|
111
|
+
// the platform probe. On the non-Linux Unix `ps` fallback path the
|
|
112
|
+
// tokens come already whitespace-split from `ps -o command=`; joining
|
|
113
|
+
// them back with spaces reproduces the exact pre-batch shape.
|
|
102
114
|
fn process_command_line(pid: u32) -> Option<String> {
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.collect::<Vec<_>>();
|
|
109
|
-
(!parts.is_empty()).then(|| parts.join(" "))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[cfg(target_os = "linux")]
|
|
113
|
-
fn process_environment(pid: u32) -> Option<String> {
|
|
114
|
-
String::from_utf8(std::fs::read(format!("/proc/{pid}/environ")).ok()?).ok()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#[cfg(not(target_os = "linux"))]
|
|
118
|
-
fn process_command_line(pid: u32) -> Option<String> {
|
|
119
|
-
let output = crate::os_probe::bounded_command_output_with_probe(
|
|
120
|
-
Command::new("ps")
|
|
121
|
-
.arg("-p")
|
|
122
|
-
.arg(pid.to_string())
|
|
123
|
-
.args(["-o", "command="]),
|
|
124
|
-
"provider_argv",
|
|
125
|
-
Some(pid),
|
|
126
|
-
)
|
|
127
|
-
.ok()?;
|
|
128
|
-
output
|
|
129
|
-
.status
|
|
130
|
-
.success()
|
|
131
|
-
.then(|| String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
132
|
-
.filter(|text| !text.is_empty())
|
|
115
|
+
let argv = crate::platform::argv::argv_tokens(pid)?;
|
|
116
|
+
if argv.is_empty() {
|
|
117
|
+
return None;
|
|
118
|
+
}
|
|
119
|
+
Some(argv.join(" "))
|
|
133
120
|
}
|
|
134
121
|
|
|
135
|
-
#[cfg(not(target_os = "linux"))]
|
|
136
122
|
fn process_environment(pid: u32) -> Option<String> {
|
|
137
|
-
|
|
138
|
-
None
|
|
123
|
+
crate::platform::argv::environ_text(pid)
|
|
139
124
|
}
|
|
140
125
|
|
|
141
126
|
#[cfg(test)]
|
|
@@ -434,7 +434,12 @@ fn execute_managed_leader_plan(
|
|
|
434
434
|
let Some(window) = plan.leader_window.as_ref() else {
|
|
435
435
|
return Err(LeaderError::Start("managed leader window missing".to_string()));
|
|
436
436
|
};
|
|
437
|
-
|
|
437
|
+
// 0.5.x Phase 1d Batch 6: use the factory tmux channel helper
|
|
438
|
+
// (thin wrapper over `TmuxBackend::for_workspace`) for
|
|
439
|
+
// grep-visibility of every intentional tmux-only leader-launcher
|
|
440
|
+
// site. Semantics unchanged; managed leader hosting stays
|
|
441
|
+
// tmux-only per design §Batch 6.
|
|
442
|
+
let transport = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
438
443
|
let spawned = ensure_managed_leader_pane(&transport, session, window, plan, workspace)?;
|
|
439
444
|
persist_managed_leader_binding(plan, workspace, &spawned)?;
|
|
440
445
|
spawn_managed_provider_startup_prompt_handler(
|
|
@@ -1029,7 +1034,9 @@ fn spawn_managed_provider_startup_prompt_handler(
|
|
|
1029
1034
|
pane_id: String,
|
|
1030
1035
|
) {
|
|
1031
1036
|
std::thread::spawn(move || {
|
|
1032
|
-
|
|
1037
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
1038
|
+
// grep-visibility. Semantics unchanged.
|
|
1039
|
+
let transport = crate::transport_factory::tmux_workspace_transport(&workspace);
|
|
1033
1040
|
let _ = handle_exec_provider_startup_prompts(
|
|
1034
1041
|
provider,
|
|
1035
1042
|
&workspace,
|
|
@@ -1042,9 +1049,13 @@ fn spawn_managed_provider_startup_prompt_handler(
|
|
|
1042
1049
|
}
|
|
1043
1050
|
|
|
1044
1051
|
fn tmux_transport_for_current_pane() -> TmuxBackend {
|
|
1052
|
+
// Phase 1d Batch 6: factory tmux channel helpers for
|
|
1053
|
+
// grep-visibility. Semantics unchanged; this is intentionally
|
|
1054
|
+
// tmux-only (caller pane = tmux `$TMUX` endpoint or default socket,
|
|
1055
|
+
// MUST-12 anchor).
|
|
1045
1056
|
crate::tmux_backend::socket_name_from_tmux_env()
|
|
1046
|
-
.map(|endpoint|
|
|
1047
|
-
.unwrap_or_else(
|
|
1057
|
+
.map(|endpoint| crate::transport_factory::tmux_endpoint_transport(&endpoint))
|
|
1058
|
+
.unwrap_or_else(crate::transport_factory::tmux_default_transport)
|
|
1048
1059
|
}
|
|
1049
1060
|
|
|
1050
1061
|
pub fn handle_exec_provider_startup_prompts(
|
|
@@ -1113,7 +1124,9 @@ fn provider_command_name(provider: Provider) -> &'static str {
|
|
|
1113
1124
|
}
|
|
1114
1125
|
|
|
1115
1126
|
fn tmux_session_exists(workspace: &Path, session: &SessionName) -> Result<bool, LeaderError> {
|
|
1116
|
-
|
|
1127
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
1128
|
+
// grep-visibility. Tmux-only anchor (managed leader session lookup).
|
|
1129
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
1117
1130
|
.has_session(session)
|
|
1118
1131
|
.map_err(|e| LeaderError::Start(format!("tmux has-session failed: {e}")))
|
|
1119
1132
|
}
|