@team-agent/installer 0.5.105 → 0.5.107
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/README.md +5 -3
- package/README.zh.md +4 -3
- package/crates/team-agent/src/cli/adapters.rs +15 -1
- package/crates/team-agent/src/cli/emit.rs +33 -13
- package/crates/team-agent/src/cli/mod.rs +148 -21
- package/crates/team-agent/src/cli/status_port/brief.rs +62 -3
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/run_delegation.rs +4 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -2
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +24 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +22 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +43 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +71 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +6 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/state/projection.rs +15 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +315 -501
- package/crates/team-agent/src/tmux_backend.rs +110 -0
- package/crates/team-agent/src/transport/test_support.rs +35 -0
- package/crates/team-agent/src/transport/tests/wire.rs +32 -58
- package/crates/team-agent/src/transport.rs +26 -12
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +2 -2
|
@@ -74,6 +74,7 @@ use crate::transport::{
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
|
|
77
|
+
pub(crate) const KEEPALIVE_WINDOW_NAME: &str = "[team-agent keepalive]";
|
|
77
78
|
const TMUX_PANE_BINDING_NONCE_OPTION: &str = "@team_agent_pane_binding_nonce";
|
|
78
79
|
const TMUX_SESSION_OWNER_WORKSPACE_OPTION: &str = "@team_agent_owner_workspace";
|
|
79
80
|
const TMUX_SESSION_OWNER_TEAM_OPTION: &str = "@team_agent_owner_team";
|
|
@@ -3875,6 +3876,26 @@ impl Transport for TmuxBackend {
|
|
|
3875
3876
|
Ok(output.stdout.trim().parse::<u8>().ok())
|
|
3876
3877
|
}
|
|
3877
3878
|
|
|
3879
|
+
fn pane_is_dead(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
|
|
3880
|
+
let argv = self.tmux_argv(&[
|
|
3881
|
+
"tmux".to_string(),
|
|
3882
|
+
"display-message".to_string(),
|
|
3883
|
+
"-p".to_string(),
|
|
3884
|
+
"-t".to_string(),
|
|
3885
|
+
pane.as_str().to_string(),
|
|
3886
|
+
"#{pane_dead}".to_string(),
|
|
3887
|
+
]);
|
|
3888
|
+
let output = self.runner.run(&argv)?;
|
|
3889
|
+
if !output.success {
|
|
3890
|
+
return Ok(None);
|
|
3891
|
+
}
|
|
3892
|
+
Ok(match output.stdout.trim() {
|
|
3893
|
+
"1" => Some(true),
|
|
3894
|
+
"0" => Some(false),
|
|
3895
|
+
_ => None,
|
|
3896
|
+
})
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3878
3899
|
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
3879
3900
|
let argv = self.tmux_argv(&[
|
|
3880
3901
|
"tmux".to_string(),
|
|
@@ -4087,6 +4108,95 @@ impl Transport for TmuxBackend {
|
|
|
4087
4108
|
self.run_ok(&argv)
|
|
4088
4109
|
}
|
|
4089
4110
|
|
|
4111
|
+
fn preserve_session_before_stop(
|
|
4112
|
+
&self,
|
|
4113
|
+
session: &SessionName,
|
|
4114
|
+
targets: &[Target],
|
|
4115
|
+
) -> Result<(), TransportError> {
|
|
4116
|
+
let list_argv = self.tmux_argv(&[
|
|
4117
|
+
"tmux".to_string(),
|
|
4118
|
+
"list-panes".to_string(),
|
|
4119
|
+
"-s".to_string(),
|
|
4120
|
+
"-t".to_string(),
|
|
4121
|
+
format!("={}", session.as_str()),
|
|
4122
|
+
"-F".to_string(),
|
|
4123
|
+
"#{pane_id}\t#{window_name}".to_string(),
|
|
4124
|
+
]);
|
|
4125
|
+
let output = self.runner.run(&list_argv)?;
|
|
4126
|
+
if !output.success {
|
|
4127
|
+
return Err(subprocess_error(list_argv, output));
|
|
4128
|
+
}
|
|
4129
|
+
for line in output.stdout.lines() {
|
|
4130
|
+
let Some((pane, window)) = line.split_once('\t') else {
|
|
4131
|
+
return Err(TransportError::Subprocess {
|
|
4132
|
+
argv: list_argv,
|
|
4133
|
+
code: output.code,
|
|
4134
|
+
stderr: "tmux returned an invalid session pane listing".to_string(),
|
|
4135
|
+
});
|
|
4136
|
+
};
|
|
4137
|
+
if !targets.iter().any(|target| match target {
|
|
4138
|
+
Target::Pane(id) => id.as_str() == pane,
|
|
4139
|
+
Target::SessionWindow {
|
|
4140
|
+
session: owner,
|
|
4141
|
+
window: name,
|
|
4142
|
+
} => owner == session && name.as_str() == window,
|
|
4143
|
+
}) {
|
|
4144
|
+
return Ok(());
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
// An empty listing cannot prove that this is the last worker.
|
|
4148
|
+
if output.stdout.trim().is_empty() {
|
|
4149
|
+
return Err(TransportError::TargetNotFound {
|
|
4150
|
+
target: session.as_str().to_string(),
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
// Use a separate, non-worker window: remain-on-exit does not protect a
|
|
4154
|
+
// pane explicitly deleted by kill-pane. Configure by the new pane ID,
|
|
4155
|
+
// not a window name that could resolve to another same-named window.
|
|
4156
|
+
let spawn_argv = tmux_spawn_argv(
|
|
4157
|
+
&SessionName::new(format!("={}", session.as_str())),
|
|
4158
|
+
&WindowName::new(KEEPALIVE_WINDOW_NAME),
|
|
4159
|
+
"exec sleep 86400",
|
|
4160
|
+
false,
|
|
4161
|
+
);
|
|
4162
|
+
let spawned = self.run_spawn(&spawn_argv)?;
|
|
4163
|
+
let pane = spawned.stdout.trim();
|
|
4164
|
+
if !pane.starts_with('%')
|
|
4165
|
+
|| pane.len() == 1
|
|
4166
|
+
|| !pane[1..].chars().all(|ch| ch.is_ascii_digit())
|
|
4167
|
+
{
|
|
4168
|
+
return Err(TransportError::Subprocess {
|
|
4169
|
+
argv: self.tmux_argv(&spawn_argv),
|
|
4170
|
+
code: spawned.code,
|
|
4171
|
+
stderr: "tmux keepalive spawn returned no valid pane id".to_string(),
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
let result = self
|
|
4175
|
+
.run_ok(&[
|
|
4176
|
+
"tmux".to_string(),
|
|
4177
|
+
"set-window-option".to_string(),
|
|
4178
|
+
"-t".to_string(),
|
|
4179
|
+
pane.to_string(),
|
|
4180
|
+
"remain-on-exit".to_string(),
|
|
4181
|
+
"on".to_string(),
|
|
4182
|
+
])
|
|
4183
|
+
.and_then(|()| {
|
|
4184
|
+
self.run_ok(&[
|
|
4185
|
+
"tmux".to_string(),
|
|
4186
|
+
"respawn-pane".to_string(),
|
|
4187
|
+
"-k".to_string(),
|
|
4188
|
+
"-t".to_string(),
|
|
4189
|
+
pane.to_string(),
|
|
4190
|
+
"exit 0".to_string(),
|
|
4191
|
+
])
|
|
4192
|
+
});
|
|
4193
|
+
if result.is_err() {
|
|
4194
|
+
// The worker has not been killed yet; remove only our new anchor.
|
|
4195
|
+
let _ = self.kill_pane(&PaneId::new(pane));
|
|
4196
|
+
}
|
|
4197
|
+
result
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4090
4200
|
fn kill_server(&self) -> Result<(), TransportError> {
|
|
4091
4201
|
TmuxBackend::kill_server(self);
|
|
4092
4202
|
Ok(())
|
|
@@ -29,6 +29,7 @@ struct OfflineState {
|
|
|
29
29
|
target_snapshots: VecDeque<Vec<PaneInfo>>,
|
|
30
30
|
windows: Vec<WindowName>,
|
|
31
31
|
pane_presence: BTreeMap<String, bool>,
|
|
32
|
+
dead_panes: BTreeMap<String, bool>,
|
|
32
33
|
spawn_failures: BTreeMap<String, String>,
|
|
33
34
|
spawned_panes_addressable: bool,
|
|
34
35
|
liveness: BTreeMap<String, PaneLiveness>,
|
|
@@ -67,6 +68,7 @@ impl Default for OfflineState {
|
|
|
67
68
|
target_snapshots: VecDeque::new(),
|
|
68
69
|
windows: Vec::new(),
|
|
69
70
|
pane_presence: BTreeMap::new(),
|
|
71
|
+
dead_panes: BTreeMap::new(),
|
|
70
72
|
spawn_failures: BTreeMap::new(),
|
|
71
73
|
spawned_panes_addressable: true,
|
|
72
74
|
liveness: BTreeMap::new(),
|
|
@@ -144,6 +146,13 @@ impl OfflineTransport {
|
|
|
144
146
|
self
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
pub fn with_pane_dead(self, pane: impl Into<String>, dead: bool) -> Self {
|
|
150
|
+
self.with_state(|state| {
|
|
151
|
+
state.dead_panes.insert(pane.into(), dead);
|
|
152
|
+
});
|
|
153
|
+
self
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
pub fn with_spawn_failure(self, window: impl Into<String>, error: impl Into<String>) -> Self {
|
|
148
157
|
self.with_state(|state| {
|
|
149
158
|
state.spawn_failures.insert(window.into(), error.into());
|
|
@@ -523,6 +532,13 @@ impl Transport for OfflineTransport {
|
|
|
523
532
|
Ok(self.with_state(|state| state.pane_current_commands.get(&pane_id).cloned()))
|
|
524
533
|
}
|
|
525
534
|
|
|
535
|
+
fn pane_is_dead(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
|
|
536
|
+
Ok(self.with_state(|state| {
|
|
537
|
+
state.calls.push("pane_is_dead");
|
|
538
|
+
state.dead_panes.get(pane.as_str()).copied()
|
|
539
|
+
}))
|
|
540
|
+
}
|
|
541
|
+
|
|
526
542
|
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
527
543
|
Ok(self.with_state(|state| {
|
|
528
544
|
state.calls.push("liveness");
|
|
@@ -582,6 +598,25 @@ impl Transport for OfflineTransport {
|
|
|
582
598
|
Ok(self.with_state(|state| state.session_owner.clone()))
|
|
583
599
|
}
|
|
584
600
|
|
|
601
|
+
fn set_session_owner_with_generation(
|
|
602
|
+
&self,
|
|
603
|
+
_session: &SessionName,
|
|
604
|
+
workspace: &Path,
|
|
605
|
+
team: &str,
|
|
606
|
+
generation: &str,
|
|
607
|
+
) -> Result<(), TransportError> {
|
|
608
|
+
self.with_state(|state| {
|
|
609
|
+
state.calls.push("set_session_owner_with_generation");
|
|
610
|
+
state.session_owner = Some(super::SessionOwner {
|
|
611
|
+
workspace: workspace.canonicalize().unwrap_or_else(|_| workspace.to_path_buf())
|
|
612
|
+
.to_string_lossy().to_string(),
|
|
613
|
+
team: team.to_string(),
|
|
614
|
+
generation: generation.to_string(),
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
Ok(())
|
|
618
|
+
}
|
|
619
|
+
|
|
585
620
|
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
586
621
|
Ok(self.with_state(|state| {
|
|
587
622
|
state.calls.push("list_windows");
|
|
@@ -195,32 +195,6 @@ fn session_window_name_transparent_bytes() {
|
|
|
195
195
|
// contracts-rust-native §2: spawn 返回的稳定 Target,后续 inject/capture 命中同进程。
|
|
196
196
|
// ════════════════════════════════════════════════════════════════════════
|
|
197
197
|
|
|
198
|
-
#[test]
|
|
199
|
-
fn target_pane_and_session_window_are_distinct_addressings() {
|
|
200
|
-
// 两种合法寻址不可混传;同名 PaneId vs SessionWindow 不相等。
|
|
201
|
-
let p = Target::Pane(PaneId::new("%7"));
|
|
202
|
-
let sw = Target::SessionWindow {
|
|
203
|
-
session: SessionName::new("team-sess"),
|
|
204
|
-
window: WindowName::new("%7"),
|
|
205
|
-
};
|
|
206
|
-
assert_ne!(p, sw);
|
|
207
|
-
// Pane 寻址按 PaneId 字节相等。
|
|
208
|
-
assert_eq!(
|
|
209
|
-
Target::Pane(PaneId::new("%7")),
|
|
210
|
-
Target::Pane(PaneId::new("%7"))
|
|
211
|
-
);
|
|
212
|
-
// SessionWindow 按 (session,window) 对相等;任一不同即不等。
|
|
213
|
-
let a = Target::SessionWindow {
|
|
214
|
-
session: SessionName::new("s"),
|
|
215
|
-
window: WindowName::new("w"),
|
|
216
|
-
};
|
|
217
|
-
let b = Target::SessionWindow {
|
|
218
|
-
session: SessionName::new("s"),
|
|
219
|
-
window: WindowName::new("w2"),
|
|
220
|
-
};
|
|
221
|
-
assert_ne!(a, b);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
198
|
#[test]
|
|
225
199
|
fn spawn_first_returns_stable_addressable_target_then_reachable() {
|
|
226
200
|
// contracts-rust-native: spawn_first 返回稳定可寻址 Target,且交回的 pane_id 能
|
|
@@ -331,16 +305,6 @@ fn spawn_into_then_list_targets_enumerates_it() {
|
|
|
331
305
|
// 会卡 trust prompt)。golden /tmp/transport_golden.py: empty_inject_*。
|
|
332
306
|
// ════════════════════════════════════════════════════════════════════════
|
|
333
307
|
|
|
334
|
-
#[test]
|
|
335
|
-
fn inject_payload_empty_is_typed_distinct_from_empty_text() {
|
|
336
|
-
// 类型上把空文本与含字符文本分流(InjectPayload::Empty != Text("..."))。
|
|
337
|
-
assert_ne!(InjectPayload::Empty, InjectPayload::Text(String::new()));
|
|
338
|
-
assert_eq!(
|
|
339
|
-
InjectPayload::Text("hi".into()),
|
|
340
|
-
InjectPayload::Text("hi".into())
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
308
|
#[test]
|
|
345
309
|
fn inject_empty_payload_reports_empty_text_send_keys_and_turn_not_required() {
|
|
346
310
|
// golden empty_inject_report:
|
|
@@ -366,16 +330,45 @@ fn inject_empty_payload_reports_empty_text_send_keys_and_turn_not_required() {
|
|
|
366
330
|
assert_eq!(report.attempts, 1);
|
|
367
331
|
}
|
|
368
332
|
|
|
333
|
+
#[cfg(unix)]
|
|
334
|
+
#[test]
|
|
335
|
+
#[ignore = "requires native tmux 3.5+ and python3; owns an isolated socket/session"]
|
|
336
|
+
fn submit_key_protocol_native_tmux_is_return_in_legacy_and_extended_modes() {
|
|
337
|
+
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
338
|
+
.join("tests/fixtures/submit_key_protocol_probe.py");
|
|
339
|
+
let output = std::process::Command::new("python3")
|
|
340
|
+
.arg(fixture)
|
|
341
|
+
.args([
|
|
342
|
+
"--submit-key",
|
|
343
|
+
crate::transport::tmux_submit_key_name(Key::Enter),
|
|
344
|
+
])
|
|
345
|
+
.output()
|
|
346
|
+
.expect("run isolated raw-PTY submit diagnostic");
|
|
347
|
+
assert!(
|
|
348
|
+
output.status.success(),
|
|
349
|
+
"production submit key is not Return: stdout={} stderr={}",
|
|
350
|
+
String::from_utf8_lossy(&output.stdout),
|
|
351
|
+
String::from_utf8_lossy(&output.stderr),
|
|
352
|
+
);
|
|
353
|
+
assert!(String::from_utf8_lossy(&output.stdout)
|
|
354
|
+
.contains("ProductionSubmitReturn=verified-three-modes"));
|
|
355
|
+
}
|
|
356
|
+
|
|
369
357
|
#[test]
|
|
370
358
|
fn empty_inject_is_single_direct_send_keys_never_buffer() {
|
|
371
359
|
// 命令构造 golden(STEP-9):空文本禁走 buffer,只发一条 send-keys 提交键
|
|
372
360
|
// (tmux_io.py:42 —— tmux 拒空 buffer 会卡 trust prompt)。
|
|
373
|
-
//
|
|
361
|
+
// Return 与 Ctrl-M 只在 legacy 模式等价;提交必须保持逻辑 Enter。
|
|
374
362
|
assert_eq!(
|
|
375
363
|
tmux_empty_inject_argv(&PaneId::new("%7"), Key::Enter),
|
|
376
|
-
vec!["tmux", "send-keys", "-t", "%7", "
|
|
364
|
+
vec!["tmux", "send-keys", "-t", "%7", "Enter"]
|
|
377
365
|
);
|
|
378
|
-
assert_eq!(
|
|
366
|
+
assert_eq!(
|
|
367
|
+
crate::transport::tmux_send_submit_argv(&PaneId::new("%7"), Key::Enter),
|
|
368
|
+
vec!["tmux", "send-keys", "-t", "%7", "Enter"]
|
|
369
|
+
);
|
|
370
|
+
assert_eq!(crate::transport::tmux_submit_key_name(Key::Enter), "Enter");
|
|
371
|
+
assert_eq!(crate::transport::tmux_submit_key_name(Key::Down), "Down");
|
|
379
372
|
}
|
|
380
373
|
|
|
381
374
|
#[test]
|
|
@@ -479,18 +472,6 @@ fn p2_inject_large_text_switches_to_load_buffer_stdin_at_16k() {
|
|
|
479
472
|
// contracts-rust-native test_key_enum_translation_parity / cancel_mode_noop。
|
|
480
473
|
// ════════════════════════════════════════════════════════════════════════
|
|
481
474
|
|
|
482
|
-
#[test]
|
|
483
|
-
fn key_enum_variants_are_distinct_and_copy() {
|
|
484
|
-
// Enter/Up/Down/Left/Right/Char/CtrlC/CancelMode 互不相等;Char 携字符。
|
|
485
|
-
assert_ne!(Key::Enter, Key::CtrlC);
|
|
486
|
-
assert_ne!(Key::Up, Key::Down);
|
|
487
|
-
assert_ne!(Key::Char('1'), Key::Char('2'));
|
|
488
|
-
assert_eq!(Key::Char('3'), Key::Char('3'));
|
|
489
|
-
// Copy:可按值传两次。
|
|
490
|
-
let k = Key::Enter;
|
|
491
|
-
let _ = (k, k);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
475
|
#[test]
|
|
495
476
|
fn send_keys_sequence_routes_through_transport() {
|
|
496
477
|
// send_keys 接受 &[Key];RED via stub。
|
|
@@ -552,13 +533,6 @@ fn cancel_mode_is_noop_on_non_tmux_backends() {
|
|
|
552
533
|
// tmux `-S -<N>`(Tail)/ `-S -`(Full);DELIVERY_CAPTURE_LINES=40 是默认 tail。
|
|
553
534
|
// ════════════════════════════════════════════════════════════════════════
|
|
554
535
|
|
|
555
|
-
#[test]
|
|
556
|
-
fn capture_range_tail_and_full_distinct() {
|
|
557
|
-
assert_ne!(CaptureRange::Tail(40), CaptureRange::Full);
|
|
558
|
-
assert_ne!(CaptureRange::Tail(40), CaptureRange::Tail(30));
|
|
559
|
-
assert_eq!(CaptureRange::Tail(40), CaptureRange::Tail(40));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
536
|
#[test]
|
|
563
537
|
fn capture_tail_records_range_in_captured_text() {
|
|
564
538
|
// CapturedText 携带它抓取的 range + 出口规范化文本。
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//! - name: tmux_inject_text_argv
|
|
14
14
|
//! what: 文本注入三步 argv(装 buffer→paste→删 buffer)的唯一构造点
|
|
15
15
|
//! - name: tmux_send_submit_argv
|
|
16
|
-
//! what: 提交键 argv 的唯一构造点,Enter
|
|
16
|
+
//! what: 提交键 argv 的唯一构造点,Enter 保持逻辑 Return 而非 Ctrl-M
|
|
17
17
|
//! - name: tmux_capture_argv
|
|
18
18
|
//! what: capture-pane argv 的唯一构造点
|
|
19
19
|
//! - name: tmux_query_argv
|
|
@@ -994,6 +994,12 @@ pub trait Transport: Send + Sync {
|
|
|
994
994
|
Ok(None)
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
+
/// Native dead-pane observation. Only Some(true) proves no live pane
|
|
998
|
+
/// process; a missing/unsupported sample must remain unknown.
|
|
999
|
+
fn pane_is_dead(&self, _pane: &PaneId) -> Result<Option<bool>, TransportError> {
|
|
1000
|
+
Ok(None)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
997
1003
|
/// Cheap direct pane existence check when a backend can prove it. `Ok(None)`
|
|
998
1004
|
/// preserves the existing Unknown boundary.
|
|
999
1005
|
fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
|
|
@@ -1085,6 +1091,17 @@ pub trait Transport: Send + Sync {
|
|
|
1085
1091
|
|
|
1086
1092
|
// —— LIFECYCLE(SL)——
|
|
1087
1093
|
|
|
1094
|
+
/// Before stopping a seat, preserve its session if removing these targets
|
|
1095
|
+
/// would otherwise destroy it. This is not called by team shutdown/restart.
|
|
1096
|
+
/// Backends whose sessions outlive their panes need no special handling.
|
|
1097
|
+
fn preserve_session_before_stop(
|
|
1098
|
+
&self,
|
|
1099
|
+
_session: &SessionName,
|
|
1100
|
+
_targets: &[Target],
|
|
1101
|
+
) -> Result<(), TransportError> {
|
|
1102
|
+
Ok(())
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1088
1105
|
fn kill_server(&self) -> Result<(), TransportError> {
|
|
1089
1106
|
Ok(())
|
|
1090
1107
|
}
|
|
@@ -1199,21 +1216,18 @@ pub fn tmux_send_keys_argv(pane: &PaneId, keys: &[Key]) -> Vec<String> {
|
|
|
1199
1216
|
|
|
1200
1217
|
/// 投递提交键的 tmux 拼写。
|
|
1201
1218
|
///
|
|
1202
|
-
///
|
|
1203
|
-
///
|
|
1204
|
-
///
|
|
1219
|
+
/// `Enter` 必须保持逻辑 Return,不能替换成控制组合 `C-m`。
|
|
1220
|
+
/// legacy PTY 中两者都是 CR,但启用 extended keys 后 `C-m` 会成为
|
|
1221
|
+
/// CSI-u / modifyOtherKeys Ctrl-M,Pi 不把它当提交;`Enter` 仍发送 CR。
|
|
1205
1222
|
/// ---
|
|
1206
|
-
/// purpose:
|
|
1223
|
+
/// purpose: 投递提交键复用通用逻辑键翻译,避免协议相关的控制键别名
|
|
1207
1224
|
/// params:
|
|
1208
|
-
/// key:
|
|
1209
|
-
/// returns:
|
|
1225
|
+
/// key: 逻辑提交键
|
|
1226
|
+
/// returns: 与 tmux_key_name 同值(含空串情形)
|
|
1210
1227
|
/// boundary: 只管拼写,不管何时提交、提交是否成功;不发送、不验证
|
|
1211
1228
|
/// ---
|
|
1212
1229
|
pub fn tmux_submit_key_name(key: Key) -> &'static str {
|
|
1213
|
-
|
|
1214
|
-
Key::Enter => "C-m",
|
|
1215
|
-
other => tmux_key_name(other),
|
|
1216
|
-
}
|
|
1230
|
+
tmux_key_name(key)
|
|
1217
1231
|
}
|
|
1218
1232
|
|
|
1219
1233
|
/// 投递提交:`send-keys -t <pane> <submit-spelling>`。
|
|
@@ -1478,7 +1492,7 @@ pub fn tmux_inject_text_argv(
|
|
|
1478
1492
|
|
|
1479
1493
|
/// 空文本 inject:纯 `send-keys -t <target> <submit_key>`,**禁** buffer 路径
|
|
1480
1494
|
/// (tmux 拒空 buffer 会卡 trust prompt;tmux_io.py:42)。
|
|
1481
|
-
/// `Key::Enter`
|
|
1495
|
+
/// `Key::Enter` 保持逻辑 `Enter`,不会在 extended keys 下误发 Ctrl-M。
|
|
1482
1496
|
/// ---
|
|
1483
1497
|
/// purpose: 空载荷注入(唤醒/ping)的 argv:只发提交键,不碰 buffer
|
|
1484
1498
|
/// params:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.107",
|
|
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.107",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.107",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.107"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -54,7 +54,7 @@ Diagnosis:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
team-agent status --workspace "$WORKSPACE" --json --detail
|
|
57
|
-
team-agent
|
|
57
|
+
team-agent inbox "$AGENT_ID" --workspace "$WORKSPACE" --team "$TEAM" -n 3 --json
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
What to look for:
|
|
@@ -167,7 +167,7 @@ Validate:
|
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
team-agent status --workspace "$WORKSPACE" --json --detail
|
|
170
|
-
team-agent
|
|
170
|
+
team-agent inbox "$AGENT_ID" --workspace "$WORKSPACE" --team "$TEAM" -n 3 --json
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
The expected result is `status="restarted"` or an equivalent running projection without duplicate-session errors. Do not treat a partial failure on one worker as permission to reset the whole team.
|