@team-agent/installer 0.3.21 → 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/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
|
@@ -23,8 +23,7 @@ pub fn restart_with_session_convergence_deadline(
|
|
|
23
23
|
session_converge_deadline_ms: Option<u64>,
|
|
24
24
|
) -> Result<RestartReport, LifecycleError> {
|
|
25
25
|
let paths = lifecycle_paths(workspace, team)?;
|
|
26
|
-
let transport =
|
|
27
|
-
lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
26
|
+
let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
28
27
|
restart_with_transport_with_session_convergence_deadline(
|
|
29
28
|
workspace,
|
|
30
29
|
allow_fresh,
|
|
@@ -148,12 +147,10 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
148
147
|
allow_fresh,
|
|
149
148
|
)?;
|
|
150
149
|
if convergence.converged && convergence.changed {
|
|
151
|
-
|
|
152
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
150
|
+
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
153
151
|
}
|
|
154
152
|
if repair_resume_sessions_from_event_log(&selected.run_workspace, &mut state)? {
|
|
155
|
-
|
|
156
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
153
|
+
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
157
154
|
let missing_after_repair = restart_required_missing_session_agent_ids(&state);
|
|
158
155
|
convergence.changed = true;
|
|
159
156
|
convergence.converged = missing_after_repair.is_empty();
|
|
@@ -173,8 +170,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
173
170
|
});
|
|
174
171
|
}
|
|
175
172
|
if !convergence.converged && convergence.changed {
|
|
176
|
-
|
|
177
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
173
|
+
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
178
174
|
}
|
|
179
175
|
let mut forced_fresh_missing = if convergence.converged {
|
|
180
176
|
std::collections::BTreeSet::new()
|
|
@@ -222,8 +218,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
222
218
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
223
219
|
mark_leader_receiver_rebind_required(&mut state, &session_name);
|
|
224
220
|
mark_restart_targets_stopped_after_teardown(&mut state, &plan.decisions);
|
|
225
|
-
|
|
226
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
221
|
+
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
227
222
|
}
|
|
228
223
|
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
229
224
|
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
@@ -353,8 +348,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
353
348
|
}
|
|
354
349
|
}
|
|
355
350
|
// END_B5_RESTART_ISOLATION_LOOP
|
|
356
|
-
|
|
357
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
351
|
+
save_restart_state(&selected.run_workspace, &mut state, &selected.team_key)?;
|
|
358
352
|
if fatal_resume_failure {
|
|
359
353
|
let attach_commands = Vec::new();
|
|
360
354
|
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
@@ -868,6 +862,14 @@ fn mark_restart_targets_stopped_after_teardown(
|
|
|
868
862
|
}
|
|
869
863
|
}
|
|
870
864
|
|
|
865
|
+
fn save_restart_state(
|
|
866
|
+
workspace: &Path,
|
|
867
|
+
state: &mut serde_json::Value,
|
|
868
|
+
team_key: &str,
|
|
869
|
+
) -> Result<(), LifecycleError> {
|
|
870
|
+
save_restart_projected_state(workspace, state, team_key)
|
|
871
|
+
}
|
|
872
|
+
|
|
871
873
|
fn mark_agent_respawned(
|
|
872
874
|
state: &mut serde_json::Value,
|
|
873
875
|
agent_id: &AgentId,
|
|
@@ -906,7 +908,13 @@ fn mark_agent_respawned(
|
|
|
906
908
|
});
|
|
907
909
|
if let Some(pane_pid) = pane_pid {
|
|
908
910
|
agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
911
|
+
} else {
|
|
912
|
+
agent.remove("pane_pid");
|
|
909
913
|
}
|
|
914
|
+
agent.insert(
|
|
915
|
+
"spawned_at".to_string(),
|
|
916
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
917
|
+
);
|
|
910
918
|
if matches!(
|
|
911
919
|
restart_mode,
|
|
912
920
|
StartMode::Fresh | StartMode::FreshAfterMissingRollout
|
|
@@ -929,7 +937,10 @@ fn mark_agent_respawned(
|
|
|
929
937
|
"layout_index".to_string(),
|
|
930
938
|
serde_json::json!(placement.layout_index),
|
|
931
939
|
);
|
|
932
|
-
agent.insert(
|
|
940
|
+
agent.insert(
|
|
941
|
+
"pane_index".to_string(),
|
|
942
|
+
serde_json::json!(placement.pane_index),
|
|
943
|
+
);
|
|
933
944
|
agent.insert(
|
|
934
945
|
"display".to_string(),
|
|
935
946
|
serde_json::json!({
|
|
@@ -963,6 +974,8 @@ fn mark_agent_respawned(
|
|
|
963
974
|
}
|
|
964
975
|
agent.remove("startup_prompts");
|
|
965
976
|
agent.remove("startup_prompt_status");
|
|
977
|
+
agent.remove("startup_prompt_probe_epoch");
|
|
978
|
+
agent.remove("startup_prompt_probe_disabled_at");
|
|
966
979
|
Ok(())
|
|
967
980
|
}
|
|
968
981
|
|
|
@@ -1403,3 +1416,272 @@ fn restart_candidate_has_context(state: &serde_json::Value) -> bool {
|
|
|
1403
1416
|
})
|
|
1404
1417
|
})
|
|
1405
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
#[cfg(test)]
|
|
1421
|
+
mod tests {
|
|
1422
|
+
use super::*;
|
|
1423
|
+
use std::collections::BTreeMap;
|
|
1424
|
+
use std::path::Path;
|
|
1425
|
+
|
|
1426
|
+
use crate::transport::{
|
|
1427
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, Key,
|
|
1428
|
+
PaneField, PaneInfo, PaneLiveness, SetEnvOutcome, SpawnResult, Target, Transport,
|
|
1429
|
+
TransportError,
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
struct RespawnEpochTransport;
|
|
1433
|
+
|
|
1434
|
+
impl Transport for RespawnEpochTransport {
|
|
1435
|
+
fn kind(&self) -> BackendKind {
|
|
1436
|
+
BackendKind::Tmux
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
fn spawn_first(
|
|
1440
|
+
&self,
|
|
1441
|
+
_session: &SessionName,
|
|
1442
|
+
_window: &WindowName,
|
|
1443
|
+
_argv: &[String],
|
|
1444
|
+
_cwd: &Path,
|
|
1445
|
+
_env: &BTreeMap<String, String>,
|
|
1446
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1447
|
+
unimplemented!("not used by respawn epoch test")
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
fn spawn_into(
|
|
1451
|
+
&self,
|
|
1452
|
+
_session: &SessionName,
|
|
1453
|
+
_window: &WindowName,
|
|
1454
|
+
_argv: &[String],
|
|
1455
|
+
_cwd: &Path,
|
|
1456
|
+
_env: &BTreeMap<String, String>,
|
|
1457
|
+
) -> Result<SpawnResult, TransportError> {
|
|
1458
|
+
unimplemented!("not used by respawn epoch test")
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
fn inject(
|
|
1462
|
+
&self,
|
|
1463
|
+
_target: &Target,
|
|
1464
|
+
_payload: &InjectPayload,
|
|
1465
|
+
_submit: Key,
|
|
1466
|
+
_bracketed: bool,
|
|
1467
|
+
) -> Result<InjectReport, TransportError> {
|
|
1468
|
+
unimplemented!("not used by respawn epoch test")
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
1472
|
+
unimplemented!("not used by respawn epoch test")
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
fn capture(
|
|
1476
|
+
&self,
|
|
1477
|
+
_target: &Target,
|
|
1478
|
+
range: CaptureRange,
|
|
1479
|
+
) -> Result<CapturedText, TransportError> {
|
|
1480
|
+
Ok(CapturedText {
|
|
1481
|
+
text: String::new(),
|
|
1482
|
+
range,
|
|
1483
|
+
})
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
fn query(
|
|
1487
|
+
&self,
|
|
1488
|
+
_target: &Target,
|
|
1489
|
+
_field: PaneField,
|
|
1490
|
+
) -> Result<Option<String>, TransportError> {
|
|
1491
|
+
Ok(None)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
1495
|
+
Ok(PaneLiveness::Live)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1499
|
+
Ok(Vec::new())
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
1503
|
+
Ok(true)
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
1507
|
+
Ok(Vec::new())
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
fn set_session_env(
|
|
1511
|
+
&self,
|
|
1512
|
+
_session: &SessionName,
|
|
1513
|
+
_key: &str,
|
|
1514
|
+
_value: &str,
|
|
1515
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
1516
|
+
Ok(SetEnvOutcome::Applied)
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
1520
|
+
Ok(())
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
1524
|
+
Ok(())
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
1528
|
+
Ok(AttachOutcome::Attached)
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
fn disabled_safety() -> DangerousApproval {
|
|
1533
|
+
DangerousApproval {
|
|
1534
|
+
enabled: false,
|
|
1535
|
+
source: DangerousApprovalSource::Disabled,
|
|
1536
|
+
inherited: false,
|
|
1537
|
+
provider: None,
|
|
1538
|
+
flag: None,
|
|
1539
|
+
worker_capability_above_leader: false,
|
|
1540
|
+
ancestry_binary_name: None,
|
|
1541
|
+
unexpected_binary: false,
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
#[test]
|
|
1546
|
+
fn respawn_refreshes_startup_probe_epoch_inputs() {
|
|
1547
|
+
let mut state = serde_json::json!({
|
|
1548
|
+
"agents": {
|
|
1549
|
+
"w1": {
|
|
1550
|
+
"status": "running",
|
|
1551
|
+
"window": "w1-old",
|
|
1552
|
+
"pane_id": "%old",
|
|
1553
|
+
"pane_pid": 1010,
|
|
1554
|
+
"spawned_at": "2026-06-01T00:00:00+00:00",
|
|
1555
|
+
"startup_prompts": "disabled_for_epoch",
|
|
1556
|
+
"startup_prompt_status": "disabled_for_epoch",
|
|
1557
|
+
"startup_prompt_probe_epoch": "pane_pid:1010",
|
|
1558
|
+
"startup_prompt_probe_disabled_at": "2026-06-01T00:02:30+00:00"
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
let spawn = SpawnedAgentWindow {
|
|
1563
|
+
spawn: SpawnResult {
|
|
1564
|
+
pane_id: PaneId::new("%new"),
|
|
1565
|
+
session: SessionName::new("team-epoch"),
|
|
1566
|
+
window: WindowName::new("w1"),
|
|
1567
|
+
child_pid: Some(2020),
|
|
1568
|
+
},
|
|
1569
|
+
plan: crate::provider::CommandPlan::argv_only(vec!["codex".to_string()]),
|
|
1570
|
+
profile_launch: crate::provider::ProviderProfileLaunch::default(),
|
|
1571
|
+
layout_placement: None,
|
|
1572
|
+
};
|
|
1573
|
+
let before = chrono::Utc::now();
|
|
1574
|
+
|
|
1575
|
+
mark_agent_respawned(
|
|
1576
|
+
&mut state,
|
|
1577
|
+
&AgentId::new("w1"),
|
|
1578
|
+
StartMode::Resumed,
|
|
1579
|
+
&spawn,
|
|
1580
|
+
&RespawnEpochTransport,
|
|
1581
|
+
&disabled_safety(),
|
|
1582
|
+
)
|
|
1583
|
+
.expect("mark respawned");
|
|
1584
|
+
|
|
1585
|
+
let agent = state.pointer("/agents/w1").expect("agent");
|
|
1586
|
+
assert_eq!(
|
|
1587
|
+
agent.get("pane_id").and_then(serde_json::Value::as_str),
|
|
1588
|
+
Some("%new")
|
|
1589
|
+
);
|
|
1590
|
+
assert_eq!(
|
|
1591
|
+
agent.get("pane_pid").and_then(serde_json::Value::as_u64),
|
|
1592
|
+
Some(2020)
|
|
1593
|
+
);
|
|
1594
|
+
let spawned_at = agent
|
|
1595
|
+
.get("spawned_at")
|
|
1596
|
+
.and_then(serde_json::Value::as_str)
|
|
1597
|
+
.expect("spawned_at refreshed");
|
|
1598
|
+
let spawned_at = chrono::DateTime::parse_from_rfc3339(spawned_at)
|
|
1599
|
+
.expect("spawned_at rfc3339")
|
|
1600
|
+
.with_timezone(&chrono::Utc);
|
|
1601
|
+
assert!(
|
|
1602
|
+
spawned_at >= before - chrono::Duration::seconds(1),
|
|
1603
|
+
"respawn must start a fresh startup-probe grace window; spawned_at={spawned_at}, before={before}"
|
|
1604
|
+
);
|
|
1605
|
+
for key in [
|
|
1606
|
+
"startup_prompts",
|
|
1607
|
+
"startup_prompt_status",
|
|
1608
|
+
"startup_prompt_probe_epoch",
|
|
1609
|
+
"startup_prompt_probe_disabled_at",
|
|
1610
|
+
] {
|
|
1611
|
+
assert!(
|
|
1612
|
+
agent.get(key).is_none(),
|
|
1613
|
+
"respawned pane must not inherit old startup prompt epoch field {key}"
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
#[test]
|
|
1619
|
+
fn restart_projection_sync_updates_active_team_and_current_alias() {
|
|
1620
|
+
let mut state = serde_json::json!({
|
|
1621
|
+
"active_team_key": "team-alpha",
|
|
1622
|
+
"team_dir": "/tmp/team-alpha",
|
|
1623
|
+
"session_name": "team-alpha",
|
|
1624
|
+
"agents": {
|
|
1625
|
+
"alpha": {
|
|
1626
|
+
"status": "running",
|
|
1627
|
+
"pane_id": "%new",
|
|
1628
|
+
"pane_pid": 47650,
|
|
1629
|
+
"spawned_at": "2026-06-13T04:00:00+00:00"
|
|
1630
|
+
}
|
|
1631
|
+
},
|
|
1632
|
+
"teams": {
|
|
1633
|
+
"current": {
|
|
1634
|
+
"team_dir": "/tmp/team-alpha",
|
|
1635
|
+
"session_name": "team-alpha",
|
|
1636
|
+
"agents": {
|
|
1637
|
+
"alpha": {
|
|
1638
|
+
"status": "running",
|
|
1639
|
+
"pane_id": "%new",
|
|
1640
|
+
"pane_pid": 47650,
|
|
1641
|
+
"spawned_at": "2026-06-13T04:00:00+00:00"
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
"team-alpha": {
|
|
1646
|
+
"team_dir": "/tmp/team-alpha",
|
|
1647
|
+
"session_name": "team-alpha",
|
|
1648
|
+
"agents": {
|
|
1649
|
+
"alpha": {
|
|
1650
|
+
"status": "running",
|
|
1651
|
+
"pane_id": "%old",
|
|
1652
|
+
"pane_pid": 46784,
|
|
1653
|
+
"spawned_at": "2026-06-13T03:00:00+00:00",
|
|
1654
|
+
"startup_prompt_probe_epoch": "pane_pid:46784"
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
sync_restart_team_projections(&mut state, "team-alpha");
|
|
1662
|
+
|
|
1663
|
+
for pointer in [
|
|
1664
|
+
"/agents/alpha",
|
|
1665
|
+
"/teams/current/agents/alpha",
|
|
1666
|
+
"/teams/team-alpha/agents/alpha",
|
|
1667
|
+
] {
|
|
1668
|
+
let agent = state.pointer(pointer).expect(pointer);
|
|
1669
|
+
assert_eq!(
|
|
1670
|
+
agent.get("pane_id").and_then(serde_json::Value::as_str),
|
|
1671
|
+
Some("%new")
|
|
1672
|
+
);
|
|
1673
|
+
assert_eq!(
|
|
1674
|
+
agent.get("pane_pid").and_then(serde_json::Value::as_u64),
|
|
1675
|
+
Some(47650)
|
|
1676
|
+
);
|
|
1677
|
+
assert_eq!(
|
|
1678
|
+
agent.get("spawned_at").and_then(serde_json::Value::as_str),
|
|
1679
|
+
Some("2026-06-13T04:00:00+00:00")
|
|
1680
|
+
);
|
|
1681
|
+
assert!(
|
|
1682
|
+
agent.get("startup_prompt_probe_epoch").is_none(),
|
|
1683
|
+
"{pointer} must not retain the old startup probe epoch"
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
@@ -1118,6 +1118,20 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1118
1118
|
vec!["spawn_first", "spawn_split", "spawn_split", "spawn_into"],
|
|
1119
1119
|
"4 default-adaptive workers should create team-w1 with 3 panes, then team-w2"
|
|
1120
1120
|
);
|
|
1121
|
+
assert_eq!(
|
|
1122
|
+
transport
|
|
1123
|
+
.pane_title_records()
|
|
1124
|
+
.iter()
|
|
1125
|
+
.map(|(_, window, pane, title)| (window.as_str(), pane.as_str(), title.as_str()))
|
|
1126
|
+
.collect::<Vec<_>>(),
|
|
1127
|
+
vec![
|
|
1128
|
+
("team-w1", "%0", "w1"),
|
|
1129
|
+
("team-w1", "%1", "w2"),
|
|
1130
|
+
("team-w1", "%2", "w3"),
|
|
1131
|
+
("team-w2", "%3", "w4"),
|
|
1132
|
+
],
|
|
1133
|
+
"adaptive workers must set tmux pane titles to the agent id"
|
|
1134
|
+
);
|
|
1121
1135
|
assert_eq!(
|
|
1122
1136
|
launch
|
|
1123
1137
|
.started
|
|
@@ -1147,6 +1161,14 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1147
1161
|
);
|
|
1148
1162
|
|
|
1149
1163
|
let (_raw, state) = raw_runtime_state(workspace);
|
|
1164
|
+
assert!(
|
|
1165
|
+
transport
|
|
1166
|
+
.pane_title_records()
|
|
1167
|
+
.iter()
|
|
1168
|
+
.all(|(session, _, _, _)| Some(session.as_str())
|
|
1169
|
+
== state.get("session_name").and_then(serde_json::Value::as_str)),
|
|
1170
|
+
"pane titles must be configured inside the team session"
|
|
1171
|
+
);
|
|
1150
1172
|
assert_eq!(state["display_backend"], json!("adaptive"));
|
|
1151
1173
|
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
1152
1174
|
assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
|
|
@@ -1155,6 +1177,14 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1155
1177
|
state.pointer("/agents/w1/display/backend"),
|
|
1156
1178
|
Some(&json!("adaptive"))
|
|
1157
1179
|
);
|
|
1180
|
+
assert_eq!(
|
|
1181
|
+
state.pointer("/agents/w1/display/pane_title"),
|
|
1182
|
+
Some(&json!("w1"))
|
|
1183
|
+
);
|
|
1184
|
+
assert_eq!(
|
|
1185
|
+
state.pointer("/agents/w1/display/linked_session"),
|
|
1186
|
+
Some(&json!(null))
|
|
1187
|
+
);
|
|
1158
1188
|
assert_eq!(
|
|
1159
1189
|
state.pointer("/agents/w4/layout_window"),
|
|
1160
1190
|
Some(&json!("team-w2"))
|
|
@@ -41,11 +41,11 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
|
|
|
41
41
|
}
|
|
42
42
|
fn spawn_first(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
43
43
|
self.spawns.lock().unwrap().push(("spawn_first".to_string(), argv.to_vec()));
|
|
44
|
-
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new("%0"), session: session.clone(), window: window.clone(), child_pid:
|
|
44
|
+
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new("%0"), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
|
|
45
45
|
}
|
|
46
46
|
fn spawn_into(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
47
47
|
self.spawns.lock().unwrap().push(("spawn_into".to_string(), argv.to_vec()));
|
|
48
|
-
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid:
|
|
48
|
+
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
|
|
49
49
|
}
|
|
50
50
|
fn inject(&self, _t: &crate::transport::Target, _p: &crate::transport::InjectPayload, _s: crate::transport::Key, _b: bool) -> Result<crate::transport::InjectReport, crate::transport::TransportError> {
|
|
51
51
|
unimplemented!("not reached by start_agent respawn")
|
|
@@ -54,16 +54,19 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
|
|
|
54
54
|
unimplemented!("not reached")
|
|
55
55
|
}
|
|
56
56
|
fn capture(&self, _t: &crate::transport::Target, _r: crate::transport::CaptureRange) -> Result<crate::transport::CapturedText, crate::transport::TransportError> {
|
|
57
|
-
|
|
57
|
+
Ok(crate::transport::CapturedText {
|
|
58
|
+
text: String::new(),
|
|
59
|
+
range: crate::transport::CaptureRange::Full,
|
|
60
|
+
})
|
|
58
61
|
}
|
|
59
62
|
fn query(&self, _t: &crate::transport::Target, _f: crate::transport::PaneField) -> Result<Option<String>, crate::transport::TransportError> {
|
|
60
63
|
unimplemented!("not reached")
|
|
61
64
|
}
|
|
62
65
|
fn liveness(&self, _p: &crate::transport::PaneId) -> Result<crate::model::enums::PaneLiveness, crate::transport::TransportError> {
|
|
63
|
-
|
|
66
|
+
Ok(crate::model::enums::PaneLiveness::Live)
|
|
64
67
|
}
|
|
65
68
|
fn list_targets(&self) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
|
|
66
|
-
|
|
69
|
+
Ok(Vec::new())
|
|
67
70
|
}
|
|
68
71
|
fn has_session(&self, _s: &crate::transport::SessionName) -> Result<bool, crate::transport::TransportError> {
|
|
69
72
|
Ok(self.session_exists)
|
|
@@ -163,6 +166,87 @@ fn reset_agent_discard_session_rebuilds_window_via_start_respawn() {
|
|
|
163
166
|
"into a LIVE session (has_session=true) the window rebuild is a new-window (spawn_into); got {recorded:?}"
|
|
164
167
|
);
|
|
165
168
|
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn reset_agent_discard_session_syncs_projection_epoch_inputs_for_restart_agent_command() {
|
|
172
|
+
let ws = restart_ws_two_resumable_workers();
|
|
173
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
174
|
+
state["team_dir"] = json!(ws.to_string_lossy());
|
|
175
|
+
state["active_team_key"] = json!("restartteam");
|
|
176
|
+
state["agents"]["alpha"]["window"] = json!("alpha");
|
|
177
|
+
state["agents"]["alpha"]["pane_id"] = json!("%old");
|
|
178
|
+
state["agents"]["alpha"]["pane_pid"] = json!(6304);
|
|
179
|
+
state["agents"]["alpha"]["spawned_at"] = json!("2026-06-13T01:00:00+00:00");
|
|
180
|
+
state["agents"]["alpha"]["startup_prompts"] = json!("disabled_for_epoch");
|
|
181
|
+
state["agents"]["alpha"]["startup_prompt_status"] = json!("disabled_for_epoch");
|
|
182
|
+
state["agents"]["alpha"]["startup_prompt_probe_epoch"] = json!("pane_pid:6304");
|
|
183
|
+
state["agents"]["alpha"]["startup_prompt_probe_disabled_at"] =
|
|
184
|
+
json!("2026-06-13T01:01:00+00:00");
|
|
185
|
+
let compact = crate::state::projection::compact_team_state(&state);
|
|
186
|
+
state["teams"] = json!({
|
|
187
|
+
"current": compact.clone(),
|
|
188
|
+
"restartteam": compact,
|
|
189
|
+
});
|
|
190
|
+
crate::state::persist::save_runtime_state(&ws, &state).expect("save stale projections");
|
|
191
|
+
|
|
192
|
+
let before = chrono::Utc::now();
|
|
193
|
+
let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
194
|
+
let transport = SessionProbeRecordingTransport {
|
|
195
|
+
spawns: std::sync::Arc::clone(&spawns),
|
|
196
|
+
session_exists: true,
|
|
197
|
+
};
|
|
198
|
+
let outcome = reset_agent_with_transport(&ws, &AgentId::new("alpha"), true, false, None, &transport)
|
|
199
|
+
.expect("reset-agent alpha --discard-session --no-display");
|
|
200
|
+
assert!(
|
|
201
|
+
matches!(outcome, ResetAgentOutcome::Reset { .. }),
|
|
202
|
+
"restart-agent alpha --discard-session --no-display must complete reset; got {outcome:?}"
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("reload state");
|
|
206
|
+
for pointer in [
|
|
207
|
+
"/agents/alpha",
|
|
208
|
+
"/teams/current/agents/alpha",
|
|
209
|
+
"/teams/restartteam/agents/alpha",
|
|
210
|
+
] {
|
|
211
|
+
let agent = state.pointer(pointer).expect(pointer);
|
|
212
|
+
assert_eq!(
|
|
213
|
+
agent.get("pane_id").and_then(serde_json::Value::as_str),
|
|
214
|
+
Some("%alpha"),
|
|
215
|
+
"{pointer} must project the reset-spawned pane, not the stale pre-reset pane"
|
|
216
|
+
);
|
|
217
|
+
assert_eq!(
|
|
218
|
+
agent.get("pane_pid").and_then(serde_json::Value::as_u64),
|
|
219
|
+
Some(6856),
|
|
220
|
+
"{pointer} must project the reset-spawned pane pid, not stale pid 6304"
|
|
221
|
+
);
|
|
222
|
+
let spawned_at = agent
|
|
223
|
+
.get("spawned_at")
|
|
224
|
+
.and_then(serde_json::Value::as_str)
|
|
225
|
+
.expect("spawned_at refreshed");
|
|
226
|
+
assert_ne!(
|
|
227
|
+
spawned_at, "2026-06-13T01:00:00+00:00",
|
|
228
|
+
"{pointer} must not retain stale spawned_at"
|
|
229
|
+
);
|
|
230
|
+
let spawned_at = chrono::DateTime::parse_from_rfc3339(spawned_at)
|
|
231
|
+
.expect("spawned_at rfc3339")
|
|
232
|
+
.with_timezone(&chrono::Utc);
|
|
233
|
+
assert!(
|
|
234
|
+
spawned_at >= before - chrono::Duration::seconds(1),
|
|
235
|
+
"{pointer} must get a fresh startup-probe grace epoch; spawned_at={spawned_at}, before={before}"
|
|
236
|
+
);
|
|
237
|
+
for key in [
|
|
238
|
+
"startup_prompts",
|
|
239
|
+
"startup_prompt_status",
|
|
240
|
+
"startup_prompt_probe_epoch",
|
|
241
|
+
"startup_prompt_probe_disabled_at",
|
|
242
|
+
] {
|
|
243
|
+
assert!(
|
|
244
|
+
agent.get(key).is_none(),
|
|
245
|
+
"{pointer} must not retain old startup probe epoch field {key}"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
166
250
|
// item ② — remove invoked with a NON-canonical input (the workspace's own `.team` subpath) must resolve
|
|
167
251
|
// the CANONICAL live run-workspace and apply the removal to the LIVE roster — not the subpath's detached
|
|
168
252
|
// runtime. canonical_run_workspace($ws/.team) == $ws. The regression made remove "succeed" against a
|