@team-agent/installer 0.5.91 → 0.5.94
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +43 -10
- package/crates/team-agent/src/cli/diagnose.rs +78 -39
- package/crates/team-agent/src/cli/mod.rs +407 -450
- package/crates/team-agent/src/cli/models.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +154 -30
- package/crates/team-agent/src/cli/triage.rs +183 -0
- package/crates/team-agent/src/coordinator/health.rs +133 -31
- package/crates/team-agent/src/coordinator/tick.rs +62 -8
- package/crates/team-agent/src/coordinator/types.rs +2 -0
- package/crates/team-agent/src/db/migration.rs +36 -15
- package/crates/team-agent/src/kill_audit.rs +41 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +48 -14
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +10 -7
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +91 -4
- package/crates/team-agent/src/platform/process.rs +4 -45
- package/crates/team-agent/src/redaction.rs +5 -1
- package/crates/team-agent/src/tmux_backend.rs +113 -3
- package/crates/team-agent/src/topology.rs +17 -11
- package/crates/team-agent/src/transport/test_support.rs +30 -0
- package/crates/team-agent/src/transport.rs +41 -0
- package/package.json +4 -4
|
@@ -5,7 +5,7 @@ use crate::cli::{
|
|
|
5
5
|
SendArgs, StatusArgs,
|
|
6
6
|
};
|
|
7
7
|
use crate::transport::test_support::OfflineTransport;
|
|
8
|
-
use crate::transport::WindowName;
|
|
8
|
+
use crate::transport::{PaneId, PaneInfo, SessionName, WindowName};
|
|
9
9
|
use serde_json::{json, Map, Value};
|
|
10
10
|
use std::collections::{BTreeMap, BTreeSet};
|
|
11
11
|
use std::path::{Path, PathBuf};
|
|
@@ -229,7 +229,21 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
229
229
|
let team = two_worker_team_dir(&hermetic);
|
|
230
230
|
let workspace = team.parent().expect("workspace").to_path_buf();
|
|
231
231
|
seed_healthy_coordinator(&workspace);
|
|
232
|
-
let
|
|
232
|
+
let _caller_pane = EnvVarGuard::set("TMUX_PANE", "%caller");
|
|
233
|
+
let launch_transport = codex_ready_transport().with_targets(vec![PaneInfo {
|
|
234
|
+
pane_id: PaneId::new("%caller"),
|
|
235
|
+
session: SessionName::new("caller"),
|
|
236
|
+
window_index: Some(0),
|
|
237
|
+
window_name: Some(WindowName::new("caller")),
|
|
238
|
+
pane_index: Some(0),
|
|
239
|
+
tty: None,
|
|
240
|
+
current_command: Some("codex".to_string()),
|
|
241
|
+
current_path: Some(workspace.clone()),
|
|
242
|
+
active: true,
|
|
243
|
+
pane_pid: None,
|
|
244
|
+
leader_env: BTreeMap::new(),
|
|
245
|
+
}])
|
|
246
|
+
.with_pane_current_command("%caller", "codex");
|
|
233
247
|
let quick_start = quick_start_with_transport_in_workspace_with_display(
|
|
234
248
|
&workspace,
|
|
235
249
|
&team,
|
|
@@ -275,9 +289,49 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
275
289
|
to_name: None,
|
|
276
290
|
to_leader: None,
|
|
277
291
|
});
|
|
292
|
+
let runtime_state = crate::state::persist::load_runtime_state(&workspace)
|
|
293
|
+
.expect("quick-start state written before shutdown fixture");
|
|
294
|
+
let generation = runtime_state
|
|
295
|
+
.get("generation")
|
|
296
|
+
.and_then(Value::as_str)
|
|
297
|
+
.or_else(|| {
|
|
298
|
+
runtime_state
|
|
299
|
+
.get("agents")
|
|
300
|
+
.and_then(Value::as_object)
|
|
301
|
+
.and_then(|agents| {
|
|
302
|
+
agents.values().find_map(|agent| {
|
|
303
|
+
agent.get("spawned_at").and_then(Value::as_str)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
.or_else(|| {
|
|
308
|
+
runtime_state
|
|
309
|
+
.pointer("/teams/teamdir/agents")
|
|
310
|
+
.and_then(Value::as_object)
|
|
311
|
+
.and_then(|agents| {
|
|
312
|
+
agents.values().find_map(|agent| {
|
|
313
|
+
agent.get("spawned_at").and_then(Value::as_str)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
.unwrap_or("phase-golden");
|
|
278
318
|
let lifecycle_transport = codex_ready_transport()
|
|
279
319
|
.with_session_present(true)
|
|
280
|
-
.with_windows(vec![WindowName::new("w1"), WindowName::new("w2")])
|
|
320
|
+
.with_windows(vec![WindowName::new("w1"), WindowName::new("w2")])
|
|
321
|
+
.with_targets(vec![PaneInfo {
|
|
322
|
+
pane_id: PaneId::new("%w1"),
|
|
323
|
+
session: SessionName::new("team-phasegolden"),
|
|
324
|
+
window_index: Some(0),
|
|
325
|
+
window_name: Some(WindowName::new("w1")),
|
|
326
|
+
pane_index: Some(0),
|
|
327
|
+
tty: None,
|
|
328
|
+
current_command: Some("codex".to_string()),
|
|
329
|
+
current_path: Some(workspace.clone()),
|
|
330
|
+
active: true,
|
|
331
|
+
pane_pid: None,
|
|
332
|
+
leader_env: BTreeMap::new(),
|
|
333
|
+
}])
|
|
334
|
+
.with_session_owner(&workspace, "teamdir", generation);
|
|
281
335
|
let lifecycle = (spec.lifecycle_op)(&workspace, &lifecycle_transport, spec.team_key);
|
|
282
336
|
let shutdown = lifecycle_port::shutdown_with_transport(
|
|
283
337
|
&workspace,
|
|
@@ -660,9 +714,12 @@ fn normalize_value(value: Value, ctx: &mut NormalizeCtx, key: Option<&str>) -> V
|
|
|
660
714
|
|
|
661
715
|
fn normalize_string(text: String, ctx: &mut NormalizeCtx, key: Option<&str>) -> Value {
|
|
662
716
|
let key = key.unwrap_or_default();
|
|
663
|
-
if key_is_timestamp(key) {
|
|
717
|
+
if key_is_timestamp(key) || key == "generation" {
|
|
664
718
|
return json!("<TS>");
|
|
665
719
|
}
|
|
720
|
+
if key == "workspace_hash" {
|
|
721
|
+
return json!("<ID>");
|
|
722
|
+
}
|
|
666
723
|
if text == crate::packaging::Version::current().as_str() {
|
|
667
724
|
return json!("<VERSION>");
|
|
668
725
|
}
|
|
@@ -696,9 +753,39 @@ fn normalize_path_string(text: &str, ctx: &NormalizeCtx) -> String {
|
|
|
696
753
|
out = out.replace(alias, "<TMP>");
|
|
697
754
|
}
|
|
698
755
|
out = normalize_tmux_socket_dir(&out);
|
|
756
|
+
out = normalize_hermetic_root(&out);
|
|
757
|
+
out = normalize_registry_hash(&out);
|
|
699
758
|
normalize_socket_token(&out)
|
|
700
759
|
}
|
|
701
760
|
|
|
761
|
+
fn normalize_registry_hash(text: &str) -> String {
|
|
762
|
+
let Some(start) = text.rfind("/leaders/") else {
|
|
763
|
+
return text.to_string();
|
|
764
|
+
};
|
|
765
|
+
let file_start = start + "/leaders/".len();
|
|
766
|
+
let Some(separator) = text[file_start..].find("__") else {
|
|
767
|
+
return text.to_string();
|
|
768
|
+
};
|
|
769
|
+
let separator = file_start + separator;
|
|
770
|
+
text[..file_start].to_owned() + "<ID>" + &text[separator..]
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fn normalize_hermetic_root(text: &str) -> String {
|
|
774
|
+
let marker = "ta-phase-golden-";
|
|
775
|
+
let Some(start) = text.find(marker) else {
|
|
776
|
+
return text.to_string();
|
|
777
|
+
};
|
|
778
|
+
let end = text[start..]
|
|
779
|
+
.find('/')
|
|
780
|
+
.map(|offset| start + offset)
|
|
781
|
+
.unwrap_or(text.len());
|
|
782
|
+
let segment = &text[start..end];
|
|
783
|
+
let Some(phase) = text[start + marker.len()..end].split('-').next() else {
|
|
784
|
+
return text.to_string();
|
|
785
|
+
};
|
|
786
|
+
text.replacen(segment, &format!("{marker}{phase}"), 1)
|
|
787
|
+
}
|
|
788
|
+
|
|
702
789
|
fn value_looks_like_endpoint_path(text: &str) -> bool {
|
|
703
790
|
text.starts_with('/')
|
|
704
791
|
|| text.starts_with("ta-")
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//! `GetExitCodeProcess`, `TerminateProcess`, Toolhelp snapshot,
|
|
13
13
|
//! Job Objects for shim-owned worker teardown)
|
|
14
14
|
//!
|
|
15
|
-
//! CR C-6: `terminate_pid`
|
|
16
|
-
//!
|
|
15
|
+
//! CR C-6: `terminate_pid` returns `TerminationOutcome` so a caller
|
|
16
|
+
//! that requested `TerminateGraceful`
|
|
17
17
|
//! can see that Windows downgraded to `TerminateForce` and emit a
|
|
18
18
|
//! `platform.terminate_force_only` event (N38 交底).
|
|
19
19
|
//!
|
|
@@ -41,7 +41,7 @@ pub enum SignalKind {
|
|
|
41
41
|
TerminateForce,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/// Result of a `terminate_pid`
|
|
44
|
+
/// Result of a `terminate_pid` call. `Graceful`
|
|
45
45
|
/// means the OS honored the request kind; `ForceOnly` means Windows
|
|
46
46
|
/// downgraded a `TerminateGraceful` to `TerminateProcess` (CR C-6
|
|
47
47
|
/// N38 交底 — caller must emit a `platform.terminate_force_only`
|
|
@@ -55,7 +55,7 @@ pub enum TerminationOutcome {
|
|
|
55
55
|
/// (no grace period). Includes a machine-readable reason so
|
|
56
56
|
/// callers can log it in the audit event.
|
|
57
57
|
ForceOnly { reason: &'static str },
|
|
58
|
-
/// The pid
|
|
58
|
+
/// The pid was already gone by the time the call resolved.
|
|
59
59
|
/// Not an error.
|
|
60
60
|
AlreadyGone,
|
|
61
61
|
}
|
|
@@ -216,34 +216,6 @@ mod unix_impl {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
/// Send a signal to a process group (`kill(-pgid, ...)`).
|
|
220
|
-
/// Byte-equivalent to `cli/mod.rs:1854` (`libc::kill(-pgid, signal)`)
|
|
221
|
-
/// used by `send_process_signal_group`.
|
|
222
|
-
pub fn terminate_group(
|
|
223
|
-
group_id: u32,
|
|
224
|
-
kind: SignalKind,
|
|
225
|
-
) -> Result<TerminationOutcome, io::Error> {
|
|
226
|
-
let signal = match kind {
|
|
227
|
-
SignalKind::TerminateGraceful => libc::SIGTERM,
|
|
228
|
-
SignalKind::TerminateForce => libc::SIGKILL,
|
|
229
|
-
};
|
|
230
|
-
let pgid_t = match libc::pid_t::try_from(group_id) {
|
|
231
|
-
Ok(p) => p,
|
|
232
|
-
Err(_) => return Ok(TerminationOutcome::AlreadyGone),
|
|
233
|
-
};
|
|
234
|
-
// `-pgid` targets every process in that group. Preserves the
|
|
235
|
-
// existing shutdown semantics inline in cli/mod.rs.
|
|
236
|
-
let rc = unsafe { libc::kill(-pgid_t, signal) };
|
|
237
|
-
if rc == 0 {
|
|
238
|
-
return Ok(TerminationOutcome::Requested);
|
|
239
|
-
}
|
|
240
|
-
let err = io::Error::last_os_error();
|
|
241
|
-
match err.raw_os_error() {
|
|
242
|
-
Some(code) if code == libc::ESRCH => Ok(TerminationOutcome::AlreadyGone),
|
|
243
|
-
_ => Err(err),
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
219
|
/// Best-effort waitpid so a killed child doesn't accumulate as a
|
|
248
220
|
/// zombie. Byte-equivalent to `coordinator/health.rs:350-358` and
|
|
249
221
|
/// `cli/mod.rs:1871-1879`.
|
|
@@ -434,19 +406,6 @@ mod windows_impl {
|
|
|
434
406
|
})
|
|
435
407
|
}
|
|
436
408
|
|
|
437
|
-
pub fn terminate_group(
|
|
438
|
-
_group_id: u32,
|
|
439
|
-
_kind: SignalKind,
|
|
440
|
-
) -> Result<TerminationOutcome, io::Error> {
|
|
441
|
-
// Windows has no `-pgid` sentinel. Design §Route B: "owned
|
|
442
|
-
// worker teardown: prefer Job Objects in the ConPTY shim".
|
|
443
|
-
// Job-object teardown is a shim-side concern, not a top-level
|
|
444
|
-
// API. Return AlreadyGone honestly here so shutdown code
|
|
445
|
-
// falls back to per-pid termination (the caller's outer
|
|
446
|
-
// loop retries per-pid after the group attempt is a no-op).
|
|
447
|
-
Ok(TerminationOutcome::AlreadyGone)
|
|
448
|
-
}
|
|
449
|
-
|
|
450
409
|
pub fn reap_child_if_possible(_pid: u32) {
|
|
451
410
|
// Windows has no zombie waitpid model. Child handles are
|
|
452
411
|
// owned by the spawner and closed at drop; nothing to do.
|
|
@@ -55,7 +55,11 @@ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Va
|
|
|
55
55
|
object
|
|
56
56
|
.iter()
|
|
57
57
|
.map(|(key, value)| {
|
|
58
|
-
let value = if
|
|
58
|
+
let value = if key.eq_ignore_ascii_case("auth")
|
|
59
|
+
&& value.as_str() == Some("unknown")
|
|
60
|
+
{
|
|
61
|
+
value.clone()
|
|
62
|
+
} else if is_sensitive_env_key(key) {
|
|
59
63
|
Value::String(REDACTED.to_string())
|
|
60
64
|
} else if is_argument_array_key(key) {
|
|
61
65
|
redact_argument_array(value)
|
|
@@ -68,13 +68,16 @@ use crate::transport::{
|
|
|
68
68
|
tmux_spawn_argv, AttachOutcome, BackendKind, BusySignalKind, CaptureRange,
|
|
69
69
|
CaptureSampleOutcome, CapturedText, InjectPayload, InjectReport, InjectStage,
|
|
70
70
|
InjectVerification, InputSurfaceProbe, Key, PaneField, PaneId, PaneInfo, PaneMode,
|
|
71
|
-
QueryOutcome, SessionName, SetEnvOutcome, SpawnResult,
|
|
72
|
-
SubmitConsumptionReason, SubmitDiagnostics, SubmitObserver,
|
|
73
|
-
Transport, TransportError, TurnVerification, WindowName,
|
|
71
|
+
QueryOutcome, SessionName, SessionOwner, SetEnvOutcome, SpawnResult,
|
|
72
|
+
SubmitAttemptObservation, SubmitConsumptionReason, SubmitDiagnostics, SubmitObserver,
|
|
73
|
+
SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
|
|
77
77
|
const TMUX_PANE_BINDING_NONCE_OPTION: &str = "@team_agent_pane_binding_nonce";
|
|
78
|
+
const TMUX_SESSION_OWNER_WORKSPACE_OPTION: &str = "@team_agent_owner_workspace";
|
|
79
|
+
const TMUX_SESSION_OWNER_TEAM_OPTION: &str = "@team_agent_owner_team";
|
|
80
|
+
const TMUX_SESSION_OWNER_GENERATION_OPTION: &str = "@team_agent_owner_generation";
|
|
78
81
|
|
|
79
82
|
/// Result of running an external command — the typed output of the OS edge.
|
|
80
83
|
#[derive(Debug, Clone)]
|
|
@@ -351,6 +354,87 @@ impl TmuxBackend {
|
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
356
|
|
|
357
|
+
fn set_session_owner_marker(
|
|
358
|
+
&self,
|
|
359
|
+
session: &SessionName,
|
|
360
|
+
workspace: &Path,
|
|
361
|
+
team: &str,
|
|
362
|
+
generation: &str,
|
|
363
|
+
) -> Result<(), TransportError> {
|
|
364
|
+
let workspace = workspace
|
|
365
|
+
.canonicalize()
|
|
366
|
+
.unwrap_or_else(|_| workspace.to_path_buf())
|
|
367
|
+
.to_string_lossy()
|
|
368
|
+
.into_owned();
|
|
369
|
+
for (option, value) in [
|
|
370
|
+
(TMUX_SESSION_OWNER_WORKSPACE_OPTION, workspace),
|
|
371
|
+
(TMUX_SESSION_OWNER_TEAM_OPTION, team.to_string()),
|
|
372
|
+
(TMUX_SESSION_OWNER_GENERATION_OPTION, generation.to_string()),
|
|
373
|
+
] {
|
|
374
|
+
let argv = self.tmux_argv(&[
|
|
375
|
+
"tmux".to_string(),
|
|
376
|
+
"set-option".to_string(),
|
|
377
|
+
"-t".to_string(),
|
|
378
|
+
session.as_str().to_string(),
|
|
379
|
+
option.to_string(),
|
|
380
|
+
value,
|
|
381
|
+
]);
|
|
382
|
+
let output = self.runner.run(&argv)?;
|
|
383
|
+
if !output.success {
|
|
384
|
+
return Err(subprocess_error(argv, output));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
Ok(())
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fn read_session_owner_option(
|
|
391
|
+
&self,
|
|
392
|
+
session: &SessionName,
|
|
393
|
+
option: &str,
|
|
394
|
+
) -> Result<Option<String>, TransportError> {
|
|
395
|
+
let argv = self.tmux_argv(&[
|
|
396
|
+
"tmux".to_string(),
|
|
397
|
+
"show-options".to_string(),
|
|
398
|
+
"-qv".to_string(),
|
|
399
|
+
"-t".to_string(),
|
|
400
|
+
session.as_str().to_string(),
|
|
401
|
+
option.to_string(),
|
|
402
|
+
]);
|
|
403
|
+
let output = self.runner.run(&argv)?;
|
|
404
|
+
if !output.success {
|
|
405
|
+
return Ok(None);
|
|
406
|
+
}
|
|
407
|
+
let value = output.stdout.trim();
|
|
408
|
+
Ok((!value.is_empty()).then(|| value.to_string()))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fn read_session_owner_marker(
|
|
412
|
+
&self,
|
|
413
|
+
session: &SessionName,
|
|
414
|
+
) -> Result<Option<SessionOwner>, TransportError> {
|
|
415
|
+
let Some(workspace) = self.read_session_owner_option(
|
|
416
|
+
session,
|
|
417
|
+
TMUX_SESSION_OWNER_WORKSPACE_OPTION,
|
|
418
|
+
)? else {
|
|
419
|
+
return Ok(None);
|
|
420
|
+
};
|
|
421
|
+
let Some(team) = self.read_session_owner_option(session, TMUX_SESSION_OWNER_TEAM_OPTION)?
|
|
422
|
+
else {
|
|
423
|
+
return Ok(None);
|
|
424
|
+
};
|
|
425
|
+
let Some(generation) = self.read_session_owner_option(
|
|
426
|
+
session,
|
|
427
|
+
TMUX_SESSION_OWNER_GENERATION_OPTION,
|
|
428
|
+
)? else {
|
|
429
|
+
return Ok(None);
|
|
430
|
+
};
|
|
431
|
+
Ok(Some(SessionOwner {
|
|
432
|
+
workspace,
|
|
433
|
+
team,
|
|
434
|
+
generation,
|
|
435
|
+
}))
|
|
436
|
+
}
|
|
437
|
+
|
|
354
438
|
/// ---
|
|
355
439
|
/// purpose: 把 leader 认领的绑定 nonce 写进 pane 级 tmux 用户选项,给「这个 pane 实例是不是我绑的那个」留可核凭据
|
|
356
440
|
/// params:
|
|
@@ -2986,6 +3070,32 @@ impl Transport for TmuxBackend {
|
|
|
2986
3070
|
.map(|endpoint| endpoint.as_endpoint().to_string())
|
|
2987
3071
|
}
|
|
2988
3072
|
|
|
3073
|
+
fn set_session_owner(
|
|
3074
|
+
&self,
|
|
3075
|
+
session: &SessionName,
|
|
3076
|
+
workspace: &Path,
|
|
3077
|
+
team: &str,
|
|
3078
|
+
) -> Result<(), TransportError> {
|
|
3079
|
+
self.set_session_owner_marker(session, workspace, team, session.as_str())
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
fn set_session_owner_with_generation(
|
|
3083
|
+
&self,
|
|
3084
|
+
session: &SessionName,
|
|
3085
|
+
workspace: &Path,
|
|
3086
|
+
team: &str,
|
|
3087
|
+
generation: &str,
|
|
3088
|
+
) -> Result<(), TransportError> {
|
|
3089
|
+
self.set_session_owner_marker(session, workspace, team, generation)
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
fn session_owner(
|
|
3093
|
+
&self,
|
|
3094
|
+
session: &SessionName,
|
|
3095
|
+
) -> Result<Option<SessionOwner>, TransportError> {
|
|
3096
|
+
self.read_session_owner_marker(session)
|
|
3097
|
+
}
|
|
3098
|
+
|
|
2989
3099
|
fn spawn_first(
|
|
2990
3100
|
&self,
|
|
2991
3101
|
session: &SessionName,
|
|
@@ -32,6 +32,19 @@ pub fn issue_id(issue: &Value) -> Option<&str> {
|
|
|
32
32
|
issue.get("id").and_then(Value::as_str)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
pub fn endpoint_socket_conflict(state: &Value) -> Option<Value> {
|
|
36
|
+
let endpoint = non_empty_str(state, "tmux_endpoint")?;
|
|
37
|
+
let socket = non_empty_str(state, "tmux_socket")?;
|
|
38
|
+
if same_endpoint(endpoint, socket) {
|
|
39
|
+
return None;
|
|
40
|
+
}
|
|
41
|
+
Some(json!({
|
|
42
|
+
"id": TMUX_ENDPOINT_SOCKET_CONFLICT,
|
|
43
|
+
"tmux_endpoint": endpoint,
|
|
44
|
+
"tmux_socket": socket,
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
36
49
|
pub enum EndpointConvergenceDecision {
|
|
37
50
|
NoConflict,
|
|
@@ -338,21 +351,14 @@ fn classify_agents_for_observed_pane(
|
|
|
338
351
|
}
|
|
339
352
|
|
|
340
353
|
fn append_socket_split_issues(state: &Value, issues: &mut Vec<Value>, include_readiness: bool) {
|
|
341
|
-
let
|
|
342
|
-
let socket = non_empty_str(state, "tmux_socket");
|
|
343
|
-
let (Some(endpoint), Some(socket)) = (endpoint, socket) else {
|
|
354
|
+
let Some(conflict) = endpoint_socket_conflict(state) else {
|
|
344
355
|
return;
|
|
345
356
|
};
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
357
|
+
let endpoint = non_empty_str(state, "tmux_endpoint").unwrap_or_default();
|
|
358
|
+
let socket = non_empty_str(state, "tmux_socket").unwrap_or_default();
|
|
349
359
|
|
|
350
360
|
let session = non_empty_str(state, "session_name").unwrap_or_default();
|
|
351
|
-
issues.push(
|
|
352
|
-
"id": TMUX_ENDPOINT_SOCKET_CONFLICT,
|
|
353
|
-
"tmux_endpoint": endpoint,
|
|
354
|
-
"tmux_socket": socket,
|
|
355
|
-
}));
|
|
361
|
+
issues.push(conflict);
|
|
356
362
|
|
|
357
363
|
if state
|
|
358
364
|
.get("leader_receiver")
|
|
@@ -55,6 +55,7 @@ struct OfflineState {
|
|
|
55
55
|
/// 0.4.x (CR C-3 / CR C-5): pre-staged `query(PaneField::PaneCurrentCommand)`
|
|
56
56
|
/// answer keyed by pane id. Used by leader provider health tests.
|
|
57
57
|
pane_current_commands: BTreeMap<String, String>,
|
|
58
|
+
session_owner: Option<super::SessionOwner>,
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
impl Default for OfflineState {
|
|
@@ -81,6 +82,7 @@ impl Default for OfflineState {
|
|
|
81
82
|
capture_text: BTreeMap::new(),
|
|
82
83
|
capture_ranges: Vec::new(),
|
|
83
84
|
pane_current_commands: BTreeMap::new(),
|
|
85
|
+
session_owner: None,
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
}
|
|
@@ -159,6 +161,27 @@ impl OfflineTransport {
|
|
|
159
161
|
self
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
pub fn with_session_owner(
|
|
165
|
+
self,
|
|
166
|
+
workspace: &Path,
|
|
167
|
+
team: impl Into<String>,
|
|
168
|
+
generation: impl Into<String>,
|
|
169
|
+
) -> Self {
|
|
170
|
+
let workspace = workspace
|
|
171
|
+
.canonicalize()
|
|
172
|
+
.unwrap_or_else(|_| workspace.to_path_buf())
|
|
173
|
+
.to_string_lossy()
|
|
174
|
+
.into_owned();
|
|
175
|
+
self.with_state(|state| {
|
|
176
|
+
state.session_owner = Some(super::SessionOwner {
|
|
177
|
+
workspace,
|
|
178
|
+
team: team.into(),
|
|
179
|
+
generation: generation.into(),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
self
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
/// U1-B: stage a `list_targets()` error to model tmux server jitter. While set,
|
|
163
186
|
/// every call returns `Err(TransportError::MuxUnavailable{detail})`. Pass an
|
|
164
187
|
/// empty string to clear via re-builder if needed.
|
|
@@ -532,6 +555,13 @@ impl Transport for OfflineTransport {
|
|
|
532
555
|
}))
|
|
533
556
|
}
|
|
534
557
|
|
|
558
|
+
fn session_owner(
|
|
559
|
+
&self,
|
|
560
|
+
_session: &SessionName,
|
|
561
|
+
) -> Result<Option<super::SessionOwner>, TransportError> {
|
|
562
|
+
Ok(self.with_state(|state| state.session_owner.clone()))
|
|
563
|
+
}
|
|
564
|
+
|
|
535
565
|
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
536
566
|
Ok(self.with_state(|state| {
|
|
537
567
|
state.calls.push("list_windows");
|
|
@@ -325,6 +325,13 @@ pub struct SpawnResult {
|
|
|
325
325
|
|
|
326
326
|
/// 全局枚举的一行(身份地基)。`leader_env` 在 tmux 后端靠反向读进程 env,在
|
|
327
327
|
/// WezTerm/ConPTY 后端靠正向登记表投影(§4a)。
|
|
328
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
329
|
+
pub struct SessionOwner {
|
|
330
|
+
pub workspace: String,
|
|
331
|
+
pub team: String,
|
|
332
|
+
pub generation: String,
|
|
333
|
+
}
|
|
334
|
+
|
|
328
335
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
329
336
|
pub struct PaneInfo {
|
|
330
337
|
pub pane_id: PaneId,
|
|
@@ -1000,6 +1007,40 @@ pub trait Transport: Send + Sync {
|
|
|
1000
1007
|
/// wezterm=`cli list --format json` + 正向登记表;conpty=daemon 内存表(仅自有,§4a)。
|
|
1001
1008
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError>;
|
|
1002
1009
|
|
|
1010
|
+
/// Write an owner marker at the session scope after a new session is created.
|
|
1011
|
+
/// Backends without a session-local metadata primitive may no-op, but then
|
|
1012
|
+
/// shutdown must treat the missing marker as unknown and refuse destructive work.
|
|
1013
|
+
fn set_session_owner(
|
|
1014
|
+
&self,
|
|
1015
|
+
_session: &SessionName,
|
|
1016
|
+
_workspace: &Path,
|
|
1017
|
+
_team: &str,
|
|
1018
|
+
) -> Result<(), TransportError> {
|
|
1019
|
+
Ok(())
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/// Write owner metadata including the launch generation when the backend
|
|
1023
|
+
/// can persist one. The default preserves compatibility for non-tmux
|
|
1024
|
+
/// transports, which remain Unknown on reads.
|
|
1025
|
+
fn set_session_owner_with_generation(
|
|
1026
|
+
&self,
|
|
1027
|
+
session: &SessionName,
|
|
1028
|
+
workspace: &Path,
|
|
1029
|
+
team: &str,
|
|
1030
|
+
_generation: &str,
|
|
1031
|
+
) -> Result<(), TransportError> {
|
|
1032
|
+
self.set_session_owner(session, workspace, team)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/// Read the session-local owner marker. `None` is an unknown/legacy session,
|
|
1036
|
+
/// never proof that the caller owns it.
|
|
1037
|
+
fn session_owner(
|
|
1038
|
+
&self,
|
|
1039
|
+
_session: &SessionName,
|
|
1040
|
+
) -> Result<Option<SessionOwner>, TransportError> {
|
|
1041
|
+
Ok(None)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1003
1044
|
fn has_session(&self, session: &SessionName) -> Result<bool, TransportError>;
|
|
1004
1045
|
|
|
1005
1046
|
fn list_windows(&self, session: &SessionName) -> Result<Vec<WindowName>, TransportError>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.94",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.94",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.94",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.94"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|