@team-agent/installer 0.3.20 → 0.3.22
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/emit.rs +9 -1
- package/crates/team-agent/src/cli/mod.rs +158 -2
- package/crates/team-agent/src/cli/status.rs +61 -0
- package/crates/team-agent/src/cli/status_port.rs +2 -2
- package/crates/team-agent/src/cli/tests/base.rs +26 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +233 -9
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +251 -2
- package/crates/team-agent/src/coordinator/tests/energy.rs +548 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +452 -17
- package/crates/team-agent/src/lifecycle/display.rs +23 -302
- package/crates/team-agent/src/lifecycle/launch.rs +38 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +27 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +76 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +295 -13
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +30 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +89 -5
- package/crates/team-agent/src/messaging/delivery.rs +324 -95
- package/crates/team-agent/src/messaging/scheduler.rs +95 -73
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +460 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +51 -2
- package/crates/team-agent/src/session_capture.rs +261 -1
- package/crates/team-agent/src/tmux_backend/tests.rs +97 -1
- package/crates/team-agent/src/tmux_backend.rs +138 -2
- package/crates/team-agent/src/transport/test_support.rs +25 -0
- package/crates/team-agent/src/transport.rs +11 -0
- package/package.json +4 -4
|
@@ -105,6 +105,7 @@ pub fn rebuild_adaptive_display_after_rebind(
|
|
|
105
105
|
struct DisplayTarget {
|
|
106
106
|
agent_id: String,
|
|
107
107
|
window: Option<WindowName>,
|
|
108
|
+
pane_id: Option<PaneId>,
|
|
108
109
|
role: Option<String>,
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -128,6 +129,11 @@ fn worker_display_targets(
|
|
|
128
129
|
targets.push(DisplayTarget {
|
|
129
130
|
agent_id: agent_id.clone(),
|
|
130
131
|
window: Some(WindowName::new(window)),
|
|
132
|
+
pane_id: agent
|
|
133
|
+
.get("pane_id")
|
|
134
|
+
.and_then(serde_json::Value::as_str)
|
|
135
|
+
.filter(|pane| !pane.is_empty())
|
|
136
|
+
.map(PaneId::new),
|
|
131
137
|
role: agent
|
|
132
138
|
.get("role")
|
|
133
139
|
.and_then(serde_json::Value::as_str)
|
|
@@ -190,81 +196,26 @@ fn open_adaptive_worker_displays(
|
|
|
190
196
|
probe: &DisplayProbe,
|
|
191
197
|
) -> Result<BTreeMap<String, WorkerDisplay>, LifecycleError> {
|
|
192
198
|
let targets = worker_display_targets(workspace, session_name)?;
|
|
193
|
-
|
|
194
|
-
return Ok(BTreeMap::new());
|
|
195
|
-
}
|
|
196
|
-
if probe.adaptive_status != DisplayStatus::Opened {
|
|
197
|
-
return Ok(targets
|
|
198
|
-
.into_iter()
|
|
199
|
-
.map(|target| {
|
|
200
|
-
(
|
|
201
|
-
target.agent_id,
|
|
202
|
-
WorkerDisplay::Blocked {
|
|
203
|
-
reason: probe
|
|
204
|
-
.reason
|
|
205
|
-
.unwrap_or(AdaptiveBlockReason::AggregatorRebuildFailed),
|
|
206
|
-
},
|
|
207
|
-
)
|
|
208
|
-
})
|
|
209
|
-
.collect());
|
|
210
|
-
}
|
|
211
|
-
let Some(leader_session) = probe.leader_session.as_ref() else {
|
|
212
|
-
return Ok(blocked_displays(
|
|
213
|
-
targets,
|
|
214
|
-
AdaptiveBlockReason::LeaderNotInTmux,
|
|
215
|
-
));
|
|
216
|
-
};
|
|
217
|
-
let mut linked_jobs = Vec::new();
|
|
218
|
-
for target in &targets {
|
|
219
|
-
match create_linked_worker_session(session_name, target) {
|
|
220
|
-
Ok(linked_session) => linked_jobs.push((target.clone(), linked_session)),
|
|
221
|
-
Err(reason) => {
|
|
222
|
-
kill_linked_sessions(linked_jobs.iter().map(|(_, linked)| linked.as_str()));
|
|
223
|
-
return Ok(blocked_displays(targets, reason));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
close_adaptive_windows(leader_session.as_str(), session_name.as_str());
|
|
228
|
-
let panes = match prepare_tmux_attached_panes(
|
|
229
|
-
leader_session.as_str(),
|
|
230
|
-
session_name.as_str(),
|
|
231
|
-
&linked_jobs,
|
|
232
|
-
) {
|
|
233
|
-
Ok(panes) => panes,
|
|
234
|
-
Err(reason) => {
|
|
235
|
-
close_adaptive_windows(leader_session.as_str(), session_name.as_str());
|
|
236
|
-
kill_linked_sessions(linked_jobs.iter().map(|(_, linked)| linked.as_str()));
|
|
237
|
-
return Ok(blocked_displays(
|
|
238
|
-
linked_jobs.into_iter().map(|(target, _)| target).collect(),
|
|
239
|
-
reason,
|
|
240
|
-
));
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
Ok(linked_jobs
|
|
199
|
+
Ok(targets
|
|
244
200
|
.into_iter()
|
|
245
|
-
.
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
target.agent_id,
|
|
201
|
+
.map(|target| {
|
|
202
|
+
let agent_id = target.agent_id.clone();
|
|
203
|
+
(
|
|
204
|
+
agent_id.clone(),
|
|
250
205
|
WorkerDisplay::Adaptive {
|
|
251
|
-
status:
|
|
252
|
-
window:
|
|
253
|
-
workspace_window:
|
|
254
|
-
pane_id:
|
|
255
|
-
pane_title: Some(
|
|
256
|
-
target:
|
|
257
|
-
target_worker_session: Some(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
linked_session: Some(linked_session),
|
|
263
|
-
leader_session: Some(leader_session.clone()),
|
|
264
|
-
display_session: Some(leader_session.clone()),
|
|
265
|
-
fallback: Some("tmux_headless".to_string()),
|
|
206
|
+
status: probe.adaptive_status,
|
|
207
|
+
window: target.window.clone(),
|
|
208
|
+
workspace_window: target.window.clone(),
|
|
209
|
+
pane_id: target.pane_id.clone(),
|
|
210
|
+
pane_title: Some(agent_id),
|
|
211
|
+
target: target.pane_id.as_ref().map(|pane| pane.as_str().to_string()),
|
|
212
|
+
target_worker_session: Some(session_name.as_str().to_string()),
|
|
213
|
+
linked_session: None,
|
|
214
|
+
leader_session: Some(session_name.clone()),
|
|
215
|
+
display_session: Some(session_name.clone()),
|
|
216
|
+
fallback: None,
|
|
266
217
|
},
|
|
267
|
-
)
|
|
218
|
+
)
|
|
268
219
|
})
|
|
269
220
|
.collect())
|
|
270
221
|
}
|
|
@@ -407,13 +358,6 @@ struct TmuxOutput {
|
|
|
407
358
|
stderr: String,
|
|
408
359
|
}
|
|
409
360
|
|
|
410
|
-
#[derive(Debug, Clone)]
|
|
411
|
-
struct PaneRecord {
|
|
412
|
-
agent_id: String,
|
|
413
|
-
pane_id: String,
|
|
414
|
-
window_name: String,
|
|
415
|
-
}
|
|
416
|
-
|
|
417
361
|
fn in_wsl() -> bool {
|
|
418
362
|
std::env::var("WSL_DISTRO_NAME").is_ok_and(|value| !value.is_empty())
|
|
419
363
|
|| std::env::var("WSL_INTEROP").is_ok_and(|value| !value.is_empty())
|
|
@@ -508,161 +452,6 @@ fn parse_tmux_info(stdout: &str) -> Option<LeaderTmuxInfo> {
|
|
|
508
452
|
}
|
|
509
453
|
}
|
|
510
454
|
|
|
511
|
-
fn create_linked_worker_session(
|
|
512
|
-
session_name: &SessionName,
|
|
513
|
-
target: &DisplayTarget,
|
|
514
|
-
) -> Result<String, AdaptiveBlockReason> {
|
|
515
|
-
let linked_session = adaptive_linked_session_name(session_name.as_str(), &target.agent_id);
|
|
516
|
-
let worker_window = target
|
|
517
|
-
.window
|
|
518
|
-
.as_ref()
|
|
519
|
-
.map(WindowName::as_str)
|
|
520
|
-
.unwrap_or(target.agent_id.as_str());
|
|
521
|
-
let _ = run_tmux(&["kill-session", "-t", linked_session.as_str()]);
|
|
522
|
-
run_tmux(&[
|
|
523
|
-
"new-session",
|
|
524
|
-
"-d",
|
|
525
|
-
"-t",
|
|
526
|
-
session_name.as_str(),
|
|
527
|
-
"-s",
|
|
528
|
-
linked_session.as_str(),
|
|
529
|
-
])
|
|
530
|
-
.map_err(|_| AdaptiveBlockReason::WorkerSessionMissing)
|
|
531
|
-
.or_else(|reason| {
|
|
532
|
-
if running_inside_tmux() {
|
|
533
|
-
Err(reason)
|
|
534
|
-
} else {
|
|
535
|
-
Ok(TmuxOutput {
|
|
536
|
-
ok: true,
|
|
537
|
-
stdout: String::new(),
|
|
538
|
-
stderr: String::new(),
|
|
539
|
-
})
|
|
540
|
-
}
|
|
541
|
-
})?;
|
|
542
|
-
if run_tmux(&[
|
|
543
|
-
"select-window",
|
|
544
|
-
"-t",
|
|
545
|
-
&format!("{linked_session}:{worker_window}"),
|
|
546
|
-
])
|
|
547
|
-
.is_err()
|
|
548
|
-
{
|
|
549
|
-
if !running_inside_tmux() {
|
|
550
|
-
return Ok(linked_session);
|
|
551
|
-
}
|
|
552
|
-
let _ = run_tmux(&["kill-session", "-t", linked_session.as_str()]);
|
|
553
|
-
return Err(AdaptiveBlockReason::WorkerSessionMissing);
|
|
554
|
-
}
|
|
555
|
-
Ok(linked_session)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
fn prepare_tmux_attached_panes(
|
|
559
|
-
leader_session: &str,
|
|
560
|
-
session_name: &str,
|
|
561
|
-
linked_jobs: &[(DisplayTarget, String)],
|
|
562
|
-
) -> Result<BTreeMap<String, PaneRecord>, AdaptiveBlockReason> {
|
|
563
|
-
let mut panes = BTreeMap::new();
|
|
564
|
-
for (window_index, window_jobs) in linked_jobs.chunks(3).enumerate() {
|
|
565
|
-
let window_name = adaptive_window_name(session_name, window_index);
|
|
566
|
-
let (first_target, first_linked_session) = &window_jobs[0];
|
|
567
|
-
let first_pane = run_tmux(&[
|
|
568
|
-
"new-window",
|
|
569
|
-
"-t",
|
|
570
|
-
leader_session,
|
|
571
|
-
"-n",
|
|
572
|
-
window_name.as_str(),
|
|
573
|
-
"-P",
|
|
574
|
-
"-F",
|
|
575
|
-
"#{pane_id}",
|
|
576
|
-
&tmux_attach_pane_command(first_linked_session),
|
|
577
|
-
]);
|
|
578
|
-
let first_pane_id = match first_pane {
|
|
579
|
-
Ok(output) => tmux_stdout_last_line(&output.stdout)
|
|
580
|
-
.unwrap_or_else(|| format!("%ta{window_index}0")),
|
|
581
|
-
Err(_) if !running_inside_tmux() => format!("%ta{window_index}0"),
|
|
582
|
-
Err(_) => return Err(AdaptiveBlockReason::WindowCreateFailed),
|
|
583
|
-
};
|
|
584
|
-
if running_inside_tmux() {
|
|
585
|
-
set_display_pane_title(&first_pane_id, first_target)?;
|
|
586
|
-
} else {
|
|
587
|
-
let _ = set_display_pane_title(&first_pane_id, first_target);
|
|
588
|
-
}
|
|
589
|
-
panes.insert(
|
|
590
|
-
first_target.agent_id.clone(),
|
|
591
|
-
PaneRecord {
|
|
592
|
-
agent_id: first_target.agent_id.clone(),
|
|
593
|
-
pane_id: first_pane_id,
|
|
594
|
-
window_name: window_name.clone(),
|
|
595
|
-
},
|
|
596
|
-
);
|
|
597
|
-
let remain = run_tmux(&[
|
|
598
|
-
"set-window-option",
|
|
599
|
-
"-t",
|
|
600
|
-
&format!("{leader_session}:{window_name}"),
|
|
601
|
-
"remain-on-exit",
|
|
602
|
-
"on",
|
|
603
|
-
]);
|
|
604
|
-
if running_inside_tmux() && remain.is_err() {
|
|
605
|
-
return Err(AdaptiveBlockReason::AggregatorRebuildFailed);
|
|
606
|
-
}
|
|
607
|
-
for (pane_index, (target, linked_session)) in window_jobs.iter().enumerate().skip(1) {
|
|
608
|
-
let split = run_tmux(&[
|
|
609
|
-
"split-window",
|
|
610
|
-
"-t",
|
|
611
|
-
&format!("{leader_session}:{window_name}"),
|
|
612
|
-
"-h",
|
|
613
|
-
"-P",
|
|
614
|
-
"-F",
|
|
615
|
-
"#{pane_id}",
|
|
616
|
-
&tmux_attach_pane_command(linked_session),
|
|
617
|
-
]);
|
|
618
|
-
let pane_id = match split {
|
|
619
|
-
Ok(output) => tmux_stdout_last_line(&output.stdout)
|
|
620
|
-
.unwrap_or_else(|| format!("%ta{window_index}{pane_index}")),
|
|
621
|
-
Err(_) if !running_inside_tmux() => format!("%ta{window_index}{pane_index}"),
|
|
622
|
-
Err(_) => return Err(AdaptiveBlockReason::SplitFailed),
|
|
623
|
-
};
|
|
624
|
-
if running_inside_tmux() {
|
|
625
|
-
set_display_pane_title(&pane_id, target)?;
|
|
626
|
-
} else {
|
|
627
|
-
let _ = set_display_pane_title(&pane_id, target);
|
|
628
|
-
}
|
|
629
|
-
panes.insert(
|
|
630
|
-
target.agent_id.clone(),
|
|
631
|
-
PaneRecord {
|
|
632
|
-
agent_id: target.agent_id.clone(),
|
|
633
|
-
pane_id,
|
|
634
|
-
window_name: window_name.clone(),
|
|
635
|
-
},
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
let layout = run_tmux(&[
|
|
639
|
-
"select-layout",
|
|
640
|
-
"-t",
|
|
641
|
-
&format!("{leader_session}:{window_name}"),
|
|
642
|
-
"even-horizontal",
|
|
643
|
-
]);
|
|
644
|
-
if running_inside_tmux() && layout.is_err() {
|
|
645
|
-
return Err(AdaptiveBlockReason::AggregatorRebuildFailed);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
Ok(panes)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
fn set_display_pane_title(
|
|
652
|
-
pane_id: &str,
|
|
653
|
-
target: &DisplayTarget,
|
|
654
|
-
) -> Result<(), AdaptiveBlockReason> {
|
|
655
|
-
run_tmux(&[
|
|
656
|
-
"select-pane",
|
|
657
|
-
"-t",
|
|
658
|
-
pane_id,
|
|
659
|
-
"-T",
|
|
660
|
-
&display_pane_title(target),
|
|
661
|
-
])
|
|
662
|
-
.map(|_| ())
|
|
663
|
-
.map_err(|_| AdaptiveBlockReason::AggregatorRebuildFailed)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
455
|
fn close_adaptive_windows(leader_session: &str, session_name: &str) -> Vec<String> {
|
|
667
456
|
let prefix = format!("team-agent:{session_name}:overview");
|
|
668
457
|
let Ok(output) = run_tmux(&["list-windows", "-t", leader_session, "-F", "#{window_name}"])
|
|
@@ -684,17 +473,6 @@ fn close_adaptive_windows(leader_session: &str, session_name: &str) -> Vec<Strin
|
|
|
684
473
|
.collect()
|
|
685
474
|
}
|
|
686
475
|
|
|
687
|
-
fn kill_linked_sessions<'a>(sessions: impl IntoIterator<Item = &'a str>) -> Vec<String> {
|
|
688
|
-
sessions
|
|
689
|
-
.into_iter()
|
|
690
|
-
.filter_map(|session| {
|
|
691
|
-
run_tmux(&["kill-session", "-t", session])
|
|
692
|
-
.ok()
|
|
693
|
-
.map(|_| session.to_string())
|
|
694
|
-
})
|
|
695
|
-
.collect()
|
|
696
|
-
}
|
|
697
|
-
|
|
698
476
|
fn kill_adaptive_window(target: &str) -> bool {
|
|
699
477
|
run_tmux(&["kill-window", "-t", target]).is_ok()
|
|
700
478
|
}
|
|
@@ -718,13 +496,6 @@ fn adaptive_leader_session(state: &serde_json::Value) -> Option<String> {
|
|
|
718
496
|
.map(str::to_string)
|
|
719
497
|
}
|
|
720
498
|
|
|
721
|
-
fn adaptive_linked_session_name(session_name: &str, agent_id: &str) -> String {
|
|
722
|
-
let digest = crate::leader::sha1_hex_prefix(format!("{session_name}:{agent_id}").as_bytes(), 8);
|
|
723
|
-
let safe_session = sanitize_tmux_name(session_name, 80, "team");
|
|
724
|
-
let safe_agent = sanitize_tmux_name(agent_id, 40, "agent");
|
|
725
|
-
format!("{safe_session}__display__{safe_agent}__{digest}")
|
|
726
|
-
}
|
|
727
|
-
|
|
728
499
|
fn adaptive_window_name(session_name: &str, index: usize) -> String {
|
|
729
500
|
if index == 0 {
|
|
730
501
|
format!("team-agent:{session_name}:overview")
|
|
@@ -738,13 +509,6 @@ fn adaptive_window_is_tagged(session_name: &str, window: &str) -> bool {
|
|
|
738
509
|
window == prefix || window.starts_with(&format!("{prefix}-"))
|
|
739
510
|
}
|
|
740
511
|
|
|
741
|
-
fn tmux_attach_pane_command(linked_session: &str) -> String {
|
|
742
|
-
format!(
|
|
743
|
-
"TMUX= tmux attach-session -t {}",
|
|
744
|
-
shell_quote(linked_session)
|
|
745
|
-
)
|
|
746
|
-
}
|
|
747
|
-
|
|
748
512
|
fn display_pane_title(target: &DisplayTarget) -> String {
|
|
749
513
|
format!(
|
|
750
514
|
"team-agent:{}:{}",
|
|
@@ -753,39 +517,6 @@ fn display_pane_title(target: &DisplayTarget) -> String {
|
|
|
753
517
|
)
|
|
754
518
|
}
|
|
755
519
|
|
|
756
|
-
fn sanitize_tmux_name(raw: &str, max_len: usize, fallback: &str) -> String {
|
|
757
|
-
let sanitized = raw
|
|
758
|
-
.chars()
|
|
759
|
-
.map(|c| {
|
|
760
|
-
if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') {
|
|
761
|
-
c
|
|
762
|
-
} else {
|
|
763
|
-
'_'
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
.collect::<String>();
|
|
767
|
-
let trimmed = sanitized
|
|
768
|
-
.chars()
|
|
769
|
-
.take(max_len)
|
|
770
|
-
.collect::<String>()
|
|
771
|
-
.trim_matches(&['.', '_', '-'][..])
|
|
772
|
-
.to_string();
|
|
773
|
-
if trimmed.is_empty() {
|
|
774
|
-
fallback.to_string()
|
|
775
|
-
} else {
|
|
776
|
-
trimmed
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
fn tmux_stdout_last_line(stdout: &str) -> Option<String> {
|
|
781
|
-
stdout
|
|
782
|
-
.lines()
|
|
783
|
-
.rev()
|
|
784
|
-
.map(str::trim)
|
|
785
|
-
.find(|line| !line.is_empty())
|
|
786
|
-
.map(str::to_string)
|
|
787
|
-
}
|
|
788
|
-
|
|
789
520
|
fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str) -> Option<String> {
|
|
790
521
|
display
|
|
791
522
|
.get(key)
|
|
@@ -814,13 +545,3 @@ fn run_tmux(args: &[&str]) -> Result<TmuxOutput, LifecycleError> {
|
|
|
814
545
|
)))
|
|
815
546
|
}
|
|
816
547
|
}
|
|
817
|
-
|
|
818
|
-
fn shell_quote(value: &str) -> String {
|
|
819
|
-
if value
|
|
820
|
-
.chars()
|
|
821
|
-
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | ':'))
|
|
822
|
-
{
|
|
823
|
-
return value.to_string();
|
|
824
|
-
}
|
|
825
|
-
format!("'{}'", value.replace('\'', "'\\''"))
|
|
826
|
-
}
|
|
@@ -438,6 +438,16 @@ fn spawn_agents(
|
|
|
438
438
|
if matches!(transport.liveness(&spawn.pane_id), Ok(PaneLiveness::Dead)) {
|
|
439
439
|
continue;
|
|
440
440
|
}
|
|
441
|
+
if placement.is_some() {
|
|
442
|
+
configure_adaptive_pane_title(
|
|
443
|
+
workspace,
|
|
444
|
+
transport,
|
|
445
|
+
session_name,
|
|
446
|
+
&window,
|
|
447
|
+
&spawn.pane_id,
|
|
448
|
+
agent_id_raw,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
441
451
|
let display = if placement.is_some() {
|
|
442
452
|
WorkerDisplay::Adaptive {
|
|
443
453
|
status: DisplayStatus::Opened,
|
|
@@ -2654,6 +2664,34 @@ pub(crate) fn selected_tmux_socket_source(
|
|
|
2654
2664
|
}
|
|
2655
2665
|
}
|
|
2656
2666
|
|
|
2667
|
+
pub(crate) fn configure_adaptive_pane_title(
|
|
2668
|
+
workspace: &Path,
|
|
2669
|
+
transport: &dyn Transport,
|
|
2670
|
+
session_name: &SessionName,
|
|
2671
|
+
window: &WindowName,
|
|
2672
|
+
pane: &PaneId,
|
|
2673
|
+
agent_id: &str,
|
|
2674
|
+
) {
|
|
2675
|
+
if let Err(error) =
|
|
2676
|
+
transport.configure_adaptive_pane_title(session_name, window, pane, agent_id)
|
|
2677
|
+
{
|
|
2678
|
+
let message = format!("adaptive layout pane title failed for {agent_id}: {error}");
|
|
2679
|
+
eprintln!("Warning: {message}");
|
|
2680
|
+
if let Err(event_error) = crate::event_log::EventLog::new(workspace).write(
|
|
2681
|
+
"adaptive_layout.pane_title_failed",
|
|
2682
|
+
serde_json::json!({
|
|
2683
|
+
"agent_id": agent_id,
|
|
2684
|
+
"session": session_name.as_str(),
|
|
2685
|
+
"window": window.as_str(),
|
|
2686
|
+
"pane_id": pane.as_str(),
|
|
2687
|
+
"warning": message,
|
|
2688
|
+
}),
|
|
2689
|
+
) {
|
|
2690
|
+
eprintln!("Warning: adaptive_layout.pane_title_failed event write failed: {event_error}");
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2657
2695
|
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
2658
2696
|
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
2659
2697
|
if workspace.is_absolute() {
|
|
@@ -97,8 +97,8 @@ pub(crate) fn start_agent_at_paths(
|
|
|
97
97
|
};
|
|
98
98
|
if !force && agent_live {
|
|
99
99
|
mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
let team_key = restart_projection_team_key(&state, team);
|
|
101
|
+
save_restart_projected_state(workspace, &mut state, &team_key)?;
|
|
102
102
|
if let Ok(spec) = load_team_spec(spec_workspace) {
|
|
103
103
|
write_team_state(spec_workspace, &spec, &state)?;
|
|
104
104
|
}
|
|
@@ -184,9 +184,9 @@ pub(crate) fn start_agent_at_paths(
|
|
|
184
184
|
layout_placement.as_ref(),
|
|
185
185
|
None,
|
|
186
186
|
)?;
|
|
187
|
-
mark_agent_started(&mut state, agent_id, &spawn_window, &spawn, &safety)?;
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
mark_agent_started(&mut state, agent_id, &spawn_window, &spawn, transport, &safety)?;
|
|
188
|
+
let team_key = restart_projection_team_key(&state, team);
|
|
189
|
+
save_restart_projected_state(workspace, &mut state, &team_key)?;
|
|
190
190
|
write_start_agent_start_event(
|
|
191
191
|
workspace,
|
|
192
192
|
agent_id,
|
|
@@ -410,6 +410,7 @@ fn mark_agent_started(
|
|
|
410
410
|
agent_id: &AgentId,
|
|
411
411
|
window: &str,
|
|
412
412
|
spawn: &SpawnedAgentWindow,
|
|
413
|
+
transport: &dyn crate::transport::Transport,
|
|
413
414
|
safety: &DangerousApproval,
|
|
414
415
|
) -> Result<(), LifecycleError> {
|
|
415
416
|
let Some(agent) = state
|
|
@@ -430,9 +431,23 @@ fn mark_agent_started(
|
|
|
430
431
|
"pane_id".to_string(),
|
|
431
432
|
serde_json::json!(spawn.spawn.pane_id.as_str()),
|
|
432
433
|
);
|
|
433
|
-
|
|
434
|
+
let pane_pid = spawn.spawn.child_pid.or_else(|| {
|
|
435
|
+
transport
|
|
436
|
+
.list_targets()
|
|
437
|
+
.unwrap_or_default()
|
|
438
|
+
.into_iter()
|
|
439
|
+
.find(|pane| pane.pane_id == spawn.spawn.pane_id)
|
|
440
|
+
.and_then(|pane| pane.pane_pid)
|
|
441
|
+
});
|
|
442
|
+
if let Some(pane_pid) = pane_pid {
|
|
434
443
|
agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
444
|
+
} else {
|
|
445
|
+
agent.remove("pane_pid");
|
|
435
446
|
}
|
|
447
|
+
agent.insert(
|
|
448
|
+
"spawned_at".to_string(),
|
|
449
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
450
|
+
);
|
|
436
451
|
crate::lifecycle::launch::persist_command_plan_state(
|
|
437
452
|
agent,
|
|
438
453
|
&spawn.plan,
|
|
@@ -467,6 +482,10 @@ fn mark_agent_started(
|
|
|
467
482
|
}),
|
|
468
483
|
);
|
|
469
484
|
}
|
|
485
|
+
agent.remove("startup_prompts");
|
|
486
|
+
agent.remove("startup_prompt_status");
|
|
487
|
+
agent.remove("startup_prompt_probe_epoch");
|
|
488
|
+
agent.remove("startup_prompt_probe_disabled_at");
|
|
470
489
|
Ok(())
|
|
471
490
|
}
|
|
472
491
|
|
|
@@ -551,6 +570,8 @@ fn reset_agent_at_paths(
|
|
|
551
570
|
let mut state = resolve_team_scoped_state_or_refuse(workspace, team)?;
|
|
552
571
|
let spec = load_team_spec(spec_workspace)?;
|
|
553
572
|
discard_agent_session_fields(&mut state, agent_id)?;
|
|
573
|
+
let team_key = restart_projection_team_key(&state, team);
|
|
574
|
+
sync_restart_team_projections(&mut state, &team_key);
|
|
554
575
|
// golden operations.py (reset): save_team_scoped_state on the team projection — same multi-team
|
|
555
576
|
// preservation as stop, not a raw save_runtime_state.
|
|
556
577
|
crate::state::projection::save_team_scoped_state_with_tombstoned_agents(
|
|
@@ -187,6 +187,16 @@ pub(super) fn spawn_agent_window(
|
|
|
187
187
|
)
|
|
188
188
|
};
|
|
189
189
|
let spawn = result.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
190
|
+
if layout_placement.is_some() {
|
|
191
|
+
crate::lifecycle::launch::configure_adaptive_pane_title(
|
|
192
|
+
workspace,
|
|
193
|
+
transport,
|
|
194
|
+
session_name,
|
|
195
|
+
&window,
|
|
196
|
+
&spawn.pane_id,
|
|
197
|
+
agent_id.as_str(),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
190
200
|
let _ = adapter.handle_startup_prompts(
|
|
191
201
|
transport,
|
|
192
202
|
&crate::transport::Target::Pane(spawn.pane_id.clone()),
|
|
@@ -247,6 +257,72 @@ pub(super) fn persist_effective_approval_policy_for_restart(
|
|
|
247
257
|
crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
pub(super) fn save_restart_projected_state(
|
|
261
|
+
workspace: &Path,
|
|
262
|
+
state: &mut serde_json::Value,
|
|
263
|
+
team_key: &str,
|
|
264
|
+
) -> Result<(), LifecycleError> {
|
|
265
|
+
sync_restart_team_projections(state, team_key);
|
|
266
|
+
crate::state::projection::save_team_scoped_state(workspace, state)
|
|
267
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pub(super) fn restart_projection_team_key(
|
|
271
|
+
state: &serde_json::Value,
|
|
272
|
+
team: Option<&str>,
|
|
273
|
+
) -> String {
|
|
274
|
+
team.filter(|key| !key.is_empty())
|
|
275
|
+
.map(str::to_string)
|
|
276
|
+
.or_else(|| {
|
|
277
|
+
state
|
|
278
|
+
.get("active_team_key")
|
|
279
|
+
.and_then(serde_json::Value::as_str)
|
|
280
|
+
.filter(|key| !key.is_empty())
|
|
281
|
+
.map(str::to_string)
|
|
282
|
+
})
|
|
283
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(state))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub(super) fn sync_restart_team_projections(state: &mut serde_json::Value, team_key: &str) {
|
|
287
|
+
let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) else {
|
|
288
|
+
return;
|
|
289
|
+
};
|
|
290
|
+
if teams.is_empty() {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
let compact = crate::state::projection::compact_team_state(state);
|
|
294
|
+
let active_key = state
|
|
295
|
+
.get("active_team_key")
|
|
296
|
+
.and_then(serde_json::Value::as_str)
|
|
297
|
+
.filter(|key| !key.is_empty())
|
|
298
|
+
.map(str::to_string);
|
|
299
|
+
let derived_key = crate::state::projection::team_state_key(state);
|
|
300
|
+
let Some(teams) = state
|
|
301
|
+
.get_mut("teams")
|
|
302
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
303
|
+
else {
|
|
304
|
+
return;
|
|
305
|
+
};
|
|
306
|
+
let mut keys = Vec::new();
|
|
307
|
+
if !team_key.is_empty() {
|
|
308
|
+
keys.push(team_key.to_string());
|
|
309
|
+
}
|
|
310
|
+
if let Some(active_key) = active_key {
|
|
311
|
+
keys.push(active_key);
|
|
312
|
+
}
|
|
313
|
+
if !derived_key.is_empty() {
|
|
314
|
+
keys.push(derived_key);
|
|
315
|
+
}
|
|
316
|
+
if teams.contains_key("current") {
|
|
317
|
+
keys.push("current".to_string());
|
|
318
|
+
}
|
|
319
|
+
keys.sort();
|
|
320
|
+
keys.dedup();
|
|
321
|
+
for key in keys {
|
|
322
|
+
teams.insert(key, compact.clone());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
250
326
|
pub(super) fn state_session_name(state: &serde_json::Value) -> SessionName {
|
|
251
327
|
state
|
|
252
328
|
.get("session_name")
|