@team-agent/installer 0.5.37 → 0.5.39
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/diagnose.rs +28 -3
- package/crates/team-agent/src/lifecycle/display.rs +58 -121
- package/crates/team-agent/src/lifecycle/launch.rs +44 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +18 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +569 -4
- package/crates/team-agent/src/lifecycle/restart.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +11 -1
- package/crates/team-agent/src/tmux_backend.rs +175 -0
- package/crates/team-agent/src/transport.rs +38 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3,6 +3,22 @@ use super::*;
|
|
|
3
3
|
use crate::provider::wire::{command_name, parse_provider, provider_wire};
|
|
4
4
|
use crate::transport::Transport;
|
|
5
5
|
|
|
6
|
+
/// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B): classify a tmux
|
|
7
|
+
/// transport error string into a diagnose issue id. When the underlying
|
|
8
|
+
/// tmux subprocess stderr contains `server exited unexpectedly`, the
|
|
9
|
+
/// tmux server itself crashed (upstream tmux 3.6a control-mode +
|
|
10
|
+
/// broadcast attach/detach jitter is one known trigger) — the physical
|
|
11
|
+
/// layer that disappeared is the whole server, not just this team's
|
|
12
|
+
/// session. Otherwise, callers keep the legacy `tmux_session_missing`
|
|
13
|
+
/// classification.
|
|
14
|
+
pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
|
|
15
|
+
if error_text.contains("server exited unexpectedly") {
|
|
16
|
+
"tmux_server_crashed"
|
|
17
|
+
} else {
|
|
18
|
+
"tmux_session_missing"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
7
23
|
let mut issues = Vec::new();
|
|
8
24
|
let mut repairs = Vec::new();
|
|
@@ -31,11 +47,20 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
31
47
|
));
|
|
32
48
|
}
|
|
33
49
|
Err(error) => {
|
|
34
|
-
|
|
50
|
+
// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B):
|
|
51
|
+
// when the transport error stderr matches "server exited
|
|
52
|
+
// unexpectedly", the physical layer that disappeared is
|
|
53
|
+
// the tmux server itself — not just this team's session.
|
|
54
|
+
// Surface `tmux_server_crashed` so the user's next
|
|
55
|
+
// action is a coordinator/host-level recovery, not a
|
|
56
|
+
// per-agent `restart <agent>`.
|
|
57
|
+
let error_str = error.to_string();
|
|
58
|
+
let issue_id = classify_tmux_server_error(&error_str);
|
|
59
|
+
issues.push(json!(issue_id));
|
|
35
60
|
let mut hint =
|
|
36
|
-
recovery_hint(session_name,
|
|
61
|
+
recovery_hint(session_name, issue_id, "team-agent diagnose");
|
|
37
62
|
if let Some(obj) = hint.as_object_mut() {
|
|
38
|
-
obj.insert("reason".to_string(), Value::String(
|
|
63
|
+
obj.insert("reason".to_string(), Value::String(error_str));
|
|
39
64
|
}
|
|
40
65
|
repairs.push(hint);
|
|
41
66
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
//! lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建。
|
|
2
|
+
//!
|
|
3
|
+
//! 0.5.39 Slice 1 (tmux-server-death-locate §7 Slice 1): all tmux
|
|
4
|
+
//! session/window/pane operations here MUST go through the workspace's
|
|
5
|
+
//! scoped `TmuxBackend` — raw `Command::new("tmux")` / ambient
|
|
6
|
+
//! `run_tmux(...)` helpers in this file inherit ambient `$TMUX` and can
|
|
7
|
+
//! kill sessions on the wrong socket. Ambient leader-pane env probes
|
|
8
|
+
//! (`display-message` reading $TMUX to discover which session invoked
|
|
9
|
+
//! us) live in `tmux_backend::probe_ambient_leader_pane_info` — that
|
|
10
|
+
//! probe is definitionally ambient (its whole job is "which session am I
|
|
11
|
+
//! in") and stays outside this module.
|
|
2
12
|
|
|
3
13
|
use std::collections::BTreeMap;
|
|
4
14
|
use std::path::Path;
|
|
5
|
-
use std::process::Command;
|
|
6
15
|
|
|
7
|
-
use crate::transport::{PaneId, SessionName, WindowName};
|
|
16
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
8
17
|
|
|
9
18
|
use super::*;
|
|
10
19
|
|
|
@@ -239,6 +248,12 @@ fn close_adaptive_displays(
|
|
|
239
248
|
orphans_cleaned: Vec::new(),
|
|
240
249
|
});
|
|
241
250
|
};
|
|
251
|
+
// 0.5.39 Slice 1: build workspace-scoped tmux transport once and
|
|
252
|
+
// route every destructive tmux op below through it (no ambient
|
|
253
|
+
// $TMUX). The kill_* Transport methods carry the workspace socket
|
|
254
|
+
// (`tmux -L <socket>`) so display cleanup cannot cross-kill a session
|
|
255
|
+
// on the user's default tmux server.
|
|
256
|
+
let transport = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
242
257
|
let mut closed = Vec::new();
|
|
243
258
|
let mut orphans_cleaned = Vec::new();
|
|
244
259
|
let mut seen = std::collections::BTreeSet::new();
|
|
@@ -258,13 +273,13 @@ fn close_adaptive_displays(
|
|
|
258
273
|
}
|
|
259
274
|
for target in display_identifiers(display, agent, agent_id, session_name, &state) {
|
|
260
275
|
if seen.insert(target.clone()) {
|
|
261
|
-
kill_display_target(&target);
|
|
276
|
+
kill_display_target(&transport, &target);
|
|
262
277
|
closed.push(target);
|
|
263
278
|
}
|
|
264
279
|
}
|
|
265
280
|
}
|
|
266
281
|
if let Some(leader_session) = adaptive_leader_session(&state) {
|
|
267
|
-
for target in close_adaptive_windows(leader_session.as_str(), session_name.as_str()) {
|
|
282
|
+
for target in close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str()) {
|
|
268
283
|
orphans_cleaned.push(target.clone());
|
|
269
284
|
if seen.insert(target.clone()) {
|
|
270
285
|
closed.push(target);
|
|
@@ -351,13 +366,6 @@ struct LeaderTmuxInfo {
|
|
|
351
366
|
pane: Option<String>,
|
|
352
367
|
}
|
|
353
368
|
|
|
354
|
-
#[derive(Debug)]
|
|
355
|
-
struct TmuxOutput {
|
|
356
|
-
ok: bool,
|
|
357
|
-
stdout: String,
|
|
358
|
-
stderr: String,
|
|
359
|
-
}
|
|
360
|
-
|
|
361
369
|
fn in_wsl() -> bool {
|
|
362
370
|
std::env::var("WSL_DISTRO_NAME").is_ok_and(|value| !value.is_empty())
|
|
363
371
|
|| std::env::var("WSL_INTEROP").is_ok_and(|value| !value.is_empty())
|
|
@@ -369,52 +377,11 @@ fn running_inside_tmux() -> bool {
|
|
|
369
377
|
}
|
|
370
378
|
|
|
371
379
|
fn current_leader_tmux_info() -> Option<LeaderTmuxInfo> {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
commands.push(vec![
|
|
378
|
-
"display-message".to_string(),
|
|
379
|
-
"-p".to_string(),
|
|
380
|
-
"-t".to_string(),
|
|
381
|
-
pane.to_string(),
|
|
382
|
-
"-F".to_string(),
|
|
383
|
-
"#{session_name}\t#{pane_id}".to_string(),
|
|
384
|
-
]);
|
|
385
|
-
commands.push(vec![
|
|
386
|
-
"display-message".to_string(),
|
|
387
|
-
"-p".to_string(),
|
|
388
|
-
"-t".to_string(),
|
|
389
|
-
pane.to_string(),
|
|
390
|
-
"-F".to_string(),
|
|
391
|
-
"#{session_name}".to_string(),
|
|
392
|
-
]);
|
|
393
|
-
}
|
|
394
|
-
if std::env::var("TMUX").is_ok_and(|value| !value.is_empty()) {
|
|
395
|
-
commands.push(vec![
|
|
396
|
-
"display-message".to_string(),
|
|
397
|
-
"-p".to_string(),
|
|
398
|
-
"-F".to_string(),
|
|
399
|
-
"#{session_name}\t#{pane_id}".to_string(),
|
|
400
|
-
]);
|
|
401
|
-
commands.push(vec![
|
|
402
|
-
"display-message".to_string(),
|
|
403
|
-
"-p".to_string(),
|
|
404
|
-
"-F".to_string(),
|
|
405
|
-
"#{session_name}".to_string(),
|
|
406
|
-
]);
|
|
407
|
-
}
|
|
408
|
-
for command in commands {
|
|
409
|
-
let args = command.iter().map(String::as_str).collect::<Vec<_>>();
|
|
410
|
-
if let Some(parsed) = run_tmux(&args)
|
|
411
|
-
.ok()
|
|
412
|
-
.and_then(|out| parse_tmux_info(&out.stdout))
|
|
413
|
-
{
|
|
414
|
-
return Some(parsed);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
None
|
|
380
|
+
// 0.5.39 Slice 1: ambient probe lives in `tmux_backend` — the single
|
|
381
|
+
// controlled exception to the "all tmux ops through socket-scoped
|
|
382
|
+
// transport" rule (its whole job is "which session am I in?").
|
|
383
|
+
crate::tmux_backend::probe_ambient_leader_pane_info()
|
|
384
|
+
.map(|(session, pane)| LeaderTmuxInfo { session, pane })
|
|
418
385
|
}
|
|
419
386
|
|
|
420
387
|
fn env_leader_tmux_info() -> Option<LeaderTmuxInfo> {
|
|
@@ -428,44 +395,29 @@ fn env_leader_tmux_info() -> Option<LeaderTmuxInfo> {
|
|
|
428
395
|
Some(LeaderTmuxInfo { session, pane })
|
|
429
396
|
}
|
|
430
397
|
|
|
431
|
-
fn
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
Some(LeaderTmuxInfo {
|
|
437
|
-
session: (*session).to_string(),
|
|
438
|
-
pane: (!pane.is_empty()).then(|| (*pane).to_string()),
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
[pane, session, ..] if pane.starts_with('%') && !session.is_empty() => {
|
|
442
|
-
Some(LeaderTmuxInfo {
|
|
443
|
-
session: (*session).to_string(),
|
|
444
|
-
pane: Some((*pane).to_string()),
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
[session] if !session.is_empty() && !session.starts_with('%') => Some(LeaderTmuxInfo {
|
|
448
|
-
session: (*session).to_string(),
|
|
449
|
-
pane: None,
|
|
450
|
-
}),
|
|
451
|
-
_ => None,
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
fn close_adaptive_windows(leader_session: &str, session_name: &str) -> Vec<String> {
|
|
398
|
+
fn close_adaptive_windows(
|
|
399
|
+
transport: &dyn Transport,
|
|
400
|
+
leader_session: &str,
|
|
401
|
+
session_name: &str,
|
|
402
|
+
) -> Vec<String> {
|
|
456
403
|
let prefix = format!("team-agent:{session_name}:overview");
|
|
457
|
-
|
|
458
|
-
|
|
404
|
+
// 0.5.39 Slice 1: `Transport::list_windows` uses the workspace socket
|
|
405
|
+
// — if `leader_session` doesn't exist on that socket, list_windows
|
|
406
|
+
// returns Err/empty and we bail without touching another server.
|
|
407
|
+
let Ok(windows) = transport.list_windows(&SessionName::new(leader_session.to_string())) else {
|
|
459
408
|
return Vec::new();
|
|
460
409
|
};
|
|
461
|
-
|
|
462
|
-
.
|
|
463
|
-
.
|
|
464
|
-
|
|
465
|
-
let window = line.trim();
|
|
410
|
+
windows
|
|
411
|
+
.into_iter()
|
|
412
|
+
.filter_map(|window| {
|
|
413
|
+
let window = window.as_str().to_string();
|
|
466
414
|
if window == prefix || window.starts_with(&format!("{prefix}-")) {
|
|
467
|
-
let
|
|
468
|
-
|
|
415
|
+
let target_string = format!("{leader_session}:{window}");
|
|
416
|
+
let target = Target::SessionWindow {
|
|
417
|
+
session: SessionName::new(leader_session.to_string()),
|
|
418
|
+
window: WindowName::new(window),
|
|
419
|
+
};
|
|
420
|
+
transport.kill_window(&target).ok().map(|_| target_string)
|
|
469
421
|
} else {
|
|
470
422
|
None
|
|
471
423
|
}
|
|
@@ -473,18 +425,23 @@ fn close_adaptive_windows(leader_session: &str, session_name: &str) -> Vec<Strin
|
|
|
473
425
|
.collect()
|
|
474
426
|
}
|
|
475
427
|
|
|
476
|
-
fn
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
428
|
+
fn kill_display_target(transport: &dyn Transport, target: &str) {
|
|
429
|
+
// 0.5.39 Slice 1: only kill window/pane — never fall back to
|
|
430
|
+
// kill-session on an ambiguous string shape. Locate §7 Slice 1:
|
|
431
|
+
// "Remove the string-shape `kill-session` fallback unless target is a
|
|
432
|
+
// proven display-only session." The old code parsed any bare
|
|
433
|
+
// non-`%`/non-`:` target as a session and issued `kill-session`,
|
|
434
|
+
// which is exactly the unbounded blast radius we're closing.
|
|
435
|
+
if let Some((session, window)) = target.split_once(':') {
|
|
436
|
+
let _ = transport.kill_window(&Target::SessionWindow {
|
|
437
|
+
session: SessionName::new(session.to_string()),
|
|
438
|
+
window: WindowName::new(window.to_string()),
|
|
439
|
+
});
|
|
483
440
|
} else if target.starts_with('%') {
|
|
484
|
-
let _ =
|
|
485
|
-
} else {
|
|
486
|
-
let _ = run_tmux(&["kill-session", "-t", target]);
|
|
441
|
+
let _ = transport.kill_pane(&PaneId::new(target.to_string()));
|
|
487
442
|
}
|
|
443
|
+
// Bare session-name form is intentionally a no-op: display cleanup
|
|
444
|
+
// must not kill a whole session on shape guesswork.
|
|
488
445
|
}
|
|
489
446
|
|
|
490
447
|
fn adaptive_leader_session(state: &serde_json::Value) -> Option<String> {
|
|
@@ -525,23 +482,3 @@ fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str)
|
|
|
525
482
|
.map(str::to_string)
|
|
526
483
|
}
|
|
527
484
|
|
|
528
|
-
fn run_tmux(args: &[&str]) -> Result<TmuxOutput, LifecycleError> {
|
|
529
|
-
let output = Command::new("tmux")
|
|
530
|
-
.args(args)
|
|
531
|
-
.output()
|
|
532
|
-
.map_err(|e| LifecycleError::StatePersist(format!("tmux {}: {e}", args.join(" "))))?;
|
|
533
|
-
let result = TmuxOutput {
|
|
534
|
-
ok: output.status.success(),
|
|
535
|
-
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
|
536
|
-
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
|
537
|
-
};
|
|
538
|
-
if result.ok {
|
|
539
|
-
Ok(result)
|
|
540
|
-
} else {
|
|
541
|
-
Err(LifecycleError::StatePersist(format!(
|
|
542
|
-
"tmux {}: {}",
|
|
543
|
-
args.join(" "),
|
|
544
|
-
result.stderr.trim()
|
|
545
|
-
)))
|
|
546
|
-
}
|
|
547
|
-
}
|
|
@@ -68,6 +68,9 @@ pub fn launch_with_transport_in_workspace(
|
|
|
68
68
|
transport: &dyn Transport,
|
|
69
69
|
) -> Result<LaunchReport, LifecycleError> {
|
|
70
70
|
let _ = skip_profile_smoke;
|
|
71
|
+
// 0.5.38 (`.team/artifacts/startup-latency-locate.md` §5): launch.phase
|
|
72
|
+
// timer with monotonic `elapsed_ms` for latency triage.
|
|
73
|
+
let phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
|
|
71
74
|
if !spec_path.exists() {
|
|
72
75
|
return Err(LifecycleError::Compile(format!(
|
|
73
76
|
"spec path not found: {}",
|
|
@@ -77,6 +80,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
77
80
|
let text = std::fs::read_to_string(spec_path)
|
|
78
81
|
.map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
|
|
79
82
|
let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
83
|
+
phase_timer.emit(workspace, "launch.phase", "compile_spec");
|
|
80
84
|
let session_name = spec_session_name(&spec);
|
|
81
85
|
let safety = effective_runtime_config(&spec)?;
|
|
82
86
|
if safety.enabled && !safety.inherited && !auto_approve && !dry_run {
|
|
@@ -102,6 +106,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
102
106
|
let started = if dry_run {
|
|
103
107
|
Vec::new()
|
|
104
108
|
} else {
|
|
109
|
+
phase_timer.emit(workspace, "launch.phase", "spawn_all");
|
|
105
110
|
let started = spawn_agents(
|
|
106
111
|
workspace,
|
|
107
112
|
spec_path,
|
|
@@ -119,6 +124,37 @@ pub fn launch_with_transport_in_workspace(
|
|
|
119
124
|
&started,
|
|
120
125
|
&safety,
|
|
121
126
|
)?;
|
|
127
|
+
// 0.5.38: per-worker timing tags (source="launch") so operators can
|
|
128
|
+
// trace which worker's spawn dominates wall time. Zeros for now on
|
|
129
|
+
// the sub-timings — Step 1 first enables shape assertion; a later
|
|
130
|
+
// slice may thread real command_plan / transport_spawn / handler
|
|
131
|
+
// timings from spawn_agents.
|
|
132
|
+
for agent in &started {
|
|
133
|
+
let provider = spec_agent_values(&spec)
|
|
134
|
+
.into_iter()
|
|
135
|
+
.find(|entry| {
|
|
136
|
+
entry
|
|
137
|
+
.get("id")
|
|
138
|
+
.and_then(Value::as_str)
|
|
139
|
+
.map(|id| id == agent.agent_id.as_str())
|
|
140
|
+
.unwrap_or(false)
|
|
141
|
+
})
|
|
142
|
+
.and_then(|entry| entry.get("provider").and_then(Value::as_str).map(str::to_string))
|
|
143
|
+
.unwrap_or_else(|| "fake".to_string());
|
|
144
|
+
crate::lifecycle::restart::write_worker_spawn_timing_event(
|
|
145
|
+
workspace,
|
|
146
|
+
phase_timer.elapsed_ms(),
|
|
147
|
+
agent.agent_id.as_str(),
|
|
148
|
+
&provider,
|
|
149
|
+
agent.start_mode,
|
|
150
|
+
"new-window",
|
|
151
|
+
0,
|
|
152
|
+
0,
|
|
153
|
+
0,
|
|
154
|
+
0,
|
|
155
|
+
"launch",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
122
158
|
started
|
|
123
159
|
};
|
|
124
160
|
// 0.3.28 Step 1: topology invariant guard (warn-only during migration).
|
|
@@ -3241,6 +3277,11 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3241
3277
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
3242
3278
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
3243
3279
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
3280
|
+
// 0.5.38 (`.team/artifacts/startup-latency-locate.md` §5): quick-start
|
|
3281
|
+
// owns the outer `launch.phase` timer so `coordinator_start` /
|
|
3282
|
+
// `readiness_wait` / `completed` fire monotonically after the inner
|
|
3283
|
+
// launch_with_transport's own `compile_spec` / `spawn_all` events.
|
|
3284
|
+
let quick_start_phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
|
|
3244
3285
|
let mut launch =
|
|
3245
3286
|
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
3246
3287
|
annotate_persisted_team_depth(
|
|
@@ -3257,6 +3298,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3257
3298
|
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
3258
3299
|
.map(|report| report.ok)
|
|
3259
3300
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
3301
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "coordinator_start");
|
|
3260
3302
|
let coordinator_action = if coordinator_started {
|
|
3261
3303
|
"coordinator started"
|
|
3262
3304
|
} else {
|
|
@@ -3269,6 +3311,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3269
3311
|
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
3270
3312
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
3271
3313
|
// bare Ready.
|
|
3314
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "readiness_wait");
|
|
3272
3315
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
3273
3316
|
let attach_windows = load_runtime_state(&workspace)
|
|
3274
3317
|
.ok()
|
|
@@ -3310,6 +3353,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3310
3353
|
.and_then(serde_json::Value::as_str)
|
|
3311
3354
|
.unwrap_or("none")
|
|
3312
3355
|
.to_string();
|
|
3356
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "completed");
|
|
3313
3357
|
Ok(QuickStartReport::Ready {
|
|
3314
3358
|
session_name,
|
|
3315
3359
|
launch: Box::new(launch),
|
|
@@ -228,25 +228,35 @@ pub(super) fn spawn_agent_window(
|
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// 0.5.39 Slice 2 (tmux-server-death-locate §7 Slice 2): route the
|
|
232
|
+
// primary worker spawn through the worker shell wrapper so provider
|
|
233
|
+
// exit leaves the pane at an interactive shell with an explicit exit
|
|
234
|
+
// marker instead of collapsing the pane into `[exited]`. This mirrors
|
|
235
|
+
// the leader wrapper's manual-tmux equivalence. The split path stays
|
|
236
|
+
// on plain spawn_split — split panes are display overlays, not the
|
|
237
|
+
// primary worker process.
|
|
238
|
+
let provider_label = crate::provider::wire::command_name(provider);
|
|
231
239
|
let result = if let Some(placement) = layout_placement {
|
|
232
240
|
if placement.starts_window {
|
|
233
241
|
if into_existing_session {
|
|
234
|
-
transport.
|
|
242
|
+
transport.spawn_into_with_worker_shell_wrapper(
|
|
235
243
|
session_name,
|
|
236
244
|
&window,
|
|
237
245
|
&plan.argv,
|
|
238
246
|
spawn_cwd,
|
|
239
247
|
&env,
|
|
240
248
|
&env_unset,
|
|
249
|
+
provider_label,
|
|
241
250
|
)
|
|
242
251
|
} else {
|
|
243
|
-
transport.
|
|
252
|
+
transport.spawn_first_with_worker_shell_wrapper(
|
|
244
253
|
session_name,
|
|
245
254
|
&window,
|
|
246
255
|
&plan.argv,
|
|
247
256
|
spawn_cwd,
|
|
248
257
|
&env,
|
|
249
258
|
&env_unset,
|
|
259
|
+
provider_label,
|
|
250
260
|
)
|
|
251
261
|
}
|
|
252
262
|
} else if !window_present_in_live(transport, session_name, &window)
|
|
@@ -264,13 +274,14 @@ pub(super) fn spawn_agent_window(
|
|
|
264
274
|
// hijack the developer worker's pane. Downgrade to spawn_into
|
|
265
275
|
// (new window named after agent_id) — canonical per-agent
|
|
266
276
|
// fallback the existing 7 workers use.
|
|
267
|
-
transport.
|
|
277
|
+
transport.spawn_into_with_worker_shell_wrapper(
|
|
268
278
|
session_name,
|
|
269
279
|
&WindowName::new(agent_id.as_str()),
|
|
270
280
|
&plan.argv,
|
|
271
281
|
spawn_cwd,
|
|
272
282
|
&env,
|
|
273
283
|
&env_unset,
|
|
284
|
+
provider_label,
|
|
274
285
|
)
|
|
275
286
|
} else {
|
|
276
287
|
// 0.3.28 Step 8: spawn_split must only fire from the display
|
|
@@ -286,22 +297,24 @@ pub(super) fn spawn_agent_window(
|
|
|
286
297
|
)
|
|
287
298
|
}
|
|
288
299
|
} else if into_existing_session {
|
|
289
|
-
transport.
|
|
300
|
+
transport.spawn_into_with_worker_shell_wrapper(
|
|
290
301
|
session_name,
|
|
291
302
|
&window,
|
|
292
303
|
&plan.argv,
|
|
293
304
|
spawn_cwd,
|
|
294
305
|
&env,
|
|
295
306
|
&env_unset,
|
|
307
|
+
provider_label,
|
|
296
308
|
)
|
|
297
309
|
} else {
|
|
298
|
-
transport.
|
|
310
|
+
transport.spawn_first_with_worker_shell_wrapper(
|
|
299
311
|
session_name,
|
|
300
312
|
&window,
|
|
301
313
|
&plan.argv,
|
|
302
314
|
spawn_cwd,
|
|
303
315
|
&env,
|
|
304
316
|
&env_unset,
|
|
317
|
+
provider_label,
|
|
305
318
|
)
|
|
306
319
|
};
|
|
307
320
|
let spawn = result.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|