@team-agent/installer 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +9 -1
- package/crates/team-agent/src/cli/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/mod.rs +93 -34
- package/crates/team-agent/src/cli/send.rs +69 -2
- package/crates/team-agent/src/cli/status_port.rs +62 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +12 -19
- package/crates/team-agent/src/coordinator/tick.rs +13 -0
- package/crates/team-agent/src/lifecycle/launch.rs +14 -6
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +209 -30
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -25
- package/crates/team-agent/src/messaging/tests/runtime.rs +446 -0
- package/crates/team-agent/src/messaging/watchers.rs +36 -19
- package/crates/team-agent/src/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +17 -0
- package/crates/team-agent/src/state/projection.rs +59 -4
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
use crate::transport::test_support::OfflineTransport;
|
|
3
|
-
use serial_test::serial;
|
|
4
3
|
use serde_json::json;
|
|
4
|
+
use serial_test::serial;
|
|
5
5
|
|
|
6
6
|
const QS_TEAM_MD: &str =
|
|
7
7
|
"---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
|
|
@@ -35,7 +35,32 @@ fn role_doc(id: &str) -> String {
|
|
|
35
35
|
)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
fn
|
|
38
|
+
fn codex_identity_rollout(
|
|
39
|
+
workspace: &std::path::Path,
|
|
40
|
+
file_name: &str,
|
|
41
|
+
session_id: &str,
|
|
42
|
+
embedded_agent_id: &str,
|
|
43
|
+
) -> PathBuf {
|
|
44
|
+
let path = workspace.join(file_name);
|
|
45
|
+
std::fs::write(
|
|
46
|
+
&path,
|
|
47
|
+
format!(
|
|
48
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
|
|
49
|
+
{{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
|
|
50
|
+
{{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
|
|
51
|
+
workspace.to_string_lossy()
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
.unwrap();
|
|
55
|
+
path
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn layout_pane(
|
|
59
|
+
session: &str,
|
|
60
|
+
window: &str,
|
|
61
|
+
pane: &str,
|
|
62
|
+
pane_index: u32,
|
|
63
|
+
) -> crate::transport::PaneInfo {
|
|
39
64
|
crate::transport::PaneInfo {
|
|
40
65
|
pane_id: crate::transport::PaneId::new(pane),
|
|
41
66
|
session: crate::transport::SessionName::new(session),
|
|
@@ -232,17 +257,16 @@ fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
|
232
257
|
let workspace = temp_ws();
|
|
233
258
|
std::fs::create_dir_all(workspace.join("agents")).unwrap();
|
|
234
259
|
std::fs::write(workspace.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
235
|
-
std::fs::write(
|
|
260
|
+
std::fs::write(
|
|
261
|
+
workspace.join("agents").join("implementer.md"),
|
|
262
|
+
QS_VALID_ROLE,
|
|
263
|
+
)
|
|
264
|
+
.unwrap();
|
|
236
265
|
seed_healthy_coordinator(&workspace);
|
|
237
266
|
let transport = OfflineTransport::new();
|
|
238
267
|
|
|
239
268
|
let report = quick_start_with_transport_in_workspace(
|
|
240
|
-
&workspace,
|
|
241
|
-
&workspace,
|
|
242
|
-
None,
|
|
243
|
-
true,
|
|
244
|
-
None,
|
|
245
|
-
&transport,
|
|
269
|
+
&workspace, &workspace, None, true, None, &transport,
|
|
246
270
|
)
|
|
247
271
|
.expect("quick-start workspace-root team should reach a report");
|
|
248
272
|
assert!(
|
|
@@ -264,7 +288,11 @@ fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
|
264
288
|
Some(workspace.to_string_lossy().as_ref()),
|
|
265
289
|
"compiled team.workspace must be the project root, not its parent"
|
|
266
290
|
);
|
|
267
|
-
for agent in spec
|
|
291
|
+
for agent in spec
|
|
292
|
+
.get("agents")
|
|
293
|
+
.and_then(crate::model::yaml::Value::as_list)
|
|
294
|
+
.unwrap()
|
|
295
|
+
{
|
|
268
296
|
assert_eq!(
|
|
269
297
|
agent
|
|
270
298
|
.get("working_directory")
|
|
@@ -1119,13 +1147,17 @@ fn adaptive_layout_plan_8_workers_is_3_3_2() {
|
|
|
1119
1147
|
assert_eq!(
|
|
1120
1148
|
windows,
|
|
1121
1149
|
vec![
|
|
1122
|
-
"team-w1", "team-w1", "team-w1", "team-w2", "team-w2", "team-w2", "team-w3",
|
|
1123
|
-
"team-w3",
|
|
1150
|
+
"team-w1", "team-w1", "team-w1", "team-w2", "team-w2", "team-w2", "team-w3", "team-w3",
|
|
1124
1151
|
]
|
|
1125
1152
|
);
|
|
1126
1153
|
let pane_counts = ["team-w1", "team-w2", "team-w3"]
|
|
1127
1154
|
.into_iter()
|
|
1128
|
-
.map(|window|
|
|
1155
|
+
.map(|window| {
|
|
1156
|
+
windows
|
|
1157
|
+
.iter()
|
|
1158
|
+
.filter(|actual| actual.as_str() == window)
|
|
1159
|
+
.count()
|
|
1160
|
+
})
|
|
1129
1161
|
.collect::<Vec<_>>();
|
|
1130
1162
|
assert_eq!(pane_counts, vec![3, 3, 2]);
|
|
1131
1163
|
assert_eq!(
|
|
@@ -1158,9 +1190,12 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1158
1190
|
.expect("quick_start_with_transport must reach Ready");
|
|
1159
1191
|
|
|
1160
1192
|
let (launch, attach_commands, display_backend) = match report {
|
|
1161
|
-
QuickStartReport::Ready {
|
|
1162
|
-
|
|
1163
|
-
|
|
1193
|
+
QuickStartReport::Ready {
|
|
1194
|
+
launch,
|
|
1195
|
+
attach_commands,
|
|
1196
|
+
display_backend,
|
|
1197
|
+
..
|
|
1198
|
+
} => (*launch, attach_commands, display_backend),
|
|
1164
1199
|
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1165
1200
|
};
|
|
1166
1201
|
// 0.3.28 Step 4b: adaptive 3-pane tiling replaced with 1-window-per-agent
|
|
@@ -1183,7 +1218,9 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
|
1183
1218
|
.pane_title_records()
|
|
1184
1219
|
.iter()
|
|
1185
1220
|
.all(|(session, _, _, _)| Some(session.as_str())
|
|
1186
|
-
== state
|
|
1221
|
+
== state
|
|
1222
|
+
.get("session_name")
|
|
1223
|
+
.and_then(serde_json::Value::as_str)),
|
|
1187
1224
|
"pane titles must be configured inside the team session"
|
|
1188
1225
|
);
|
|
1189
1226
|
// Each worker lives in its OWN window named `agent_id` (Python parity).
|
|
@@ -1226,7 +1263,9 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
|
1226
1263
|
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1227
1264
|
.expect("quick_start_with_transport must reach Ready");
|
|
1228
1265
|
let attach_commands = match report {
|
|
1229
|
-
QuickStartReport::Ready {
|
|
1266
|
+
QuickStartReport::Ready {
|
|
1267
|
+
attach_commands, ..
|
|
1268
|
+
} => attach_commands,
|
|
1230
1269
|
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1231
1270
|
};
|
|
1232
1271
|
|
|
@@ -1244,7 +1283,10 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
|
1244
1283
|
assert_eq!(state["tmux_endpoint"], json!(endpoint));
|
|
1245
1284
|
assert_eq!(state["tmux_socket"], json!(endpoint));
|
|
1246
1285
|
assert_eq!(state["is_external_leader"], json!(false));
|
|
1247
|
-
assert_eq!(
|
|
1286
|
+
assert_eq!(
|
|
1287
|
+
state["teams"]["teamdir"]["is_external_leader"],
|
|
1288
|
+
json!(false)
|
|
1289
|
+
);
|
|
1248
1290
|
}
|
|
1249
1291
|
|
|
1250
1292
|
#[test]
|
|
@@ -1264,8 +1306,7 @@ fn annotate_runtime_tmux_endpoint_persists_workspace_socket_as_full_path() {
|
|
|
1264
1306
|
"runtime endpoint state must persist the full tmux socket path, not the short -L name"
|
|
1265
1307
|
);
|
|
1266
1308
|
assert_eq!(
|
|
1267
|
-
state["tmux_socket"],
|
|
1268
|
-
state["tmux_endpoint"],
|
|
1309
|
+
state["tmux_socket"], state["tmux_endpoint"],
|
|
1269
1310
|
"tmux_socket mirrors the canonical persisted endpoint"
|
|
1270
1311
|
);
|
|
1271
1312
|
}
|
|
@@ -1301,7 +1342,9 @@ fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command
|
|
|
1301
1342
|
let report = quick_start_with_transport(&team, None, true, None, &transport)
|
|
1302
1343
|
.expect("quick_start_with_transport must reach Ready");
|
|
1303
1344
|
let attach_commands = match report {
|
|
1304
|
-
QuickStartReport::Ready {
|
|
1345
|
+
QuickStartReport::Ready {
|
|
1346
|
+
attach_commands, ..
|
|
1347
|
+
} => attach_commands,
|
|
1305
1348
|
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1306
1349
|
};
|
|
1307
1350
|
|
|
@@ -1381,7 +1424,9 @@ fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
|
1381
1424
|
.expect("quick_start_with_transport must reach Ready");
|
|
1382
1425
|
|
|
1383
1426
|
let display_backend = match report {
|
|
1384
|
-
QuickStartReport::Ready {
|
|
1427
|
+
QuickStartReport::Ready {
|
|
1428
|
+
display_backend, ..
|
|
1429
|
+
} => display_backend,
|
|
1385
1430
|
other => panic!("quick_start must reach Ready; got {other:?}"),
|
|
1386
1431
|
};
|
|
1387
1432
|
assert_eq!(display_backend, "none");
|
|
@@ -1690,9 +1735,7 @@ fn restart_allow_fresh_does_not_force_fresh_other_agents_when_one_session_captur
|
|
|
1690
1735
|
std::fs::create_dir_all(&dated).unwrap();
|
|
1691
1736
|
let alpha_session = "019ee540-37ed-7a20-a141-1d654224d209";
|
|
1692
1737
|
std::fs::write(
|
|
1693
|
-
dated.join(format!(
|
|
1694
|
-
"rollout-2026-06-20T21-37-31-{alpha_session}.jsonl"
|
|
1695
|
-
)),
|
|
1738
|
+
dated.join(format!("rollout-2026-06-20T21-37-31-{alpha_session}.jsonl")),
|
|
1696
1739
|
"{}\n",
|
|
1697
1740
|
)
|
|
1698
1741
|
.unwrap();
|
|
@@ -1774,6 +1817,153 @@ fn restart_allow_fresh_does_not_force_fresh_other_agents_when_one_session_captur
|
|
|
1774
1817
|
);
|
|
1775
1818
|
}
|
|
1776
1819
|
|
|
1820
|
+
#[test]
|
|
1821
|
+
fn restart_allow_fresh_clears_codex_identity_mismatch_tuple_before_capture() {
|
|
1822
|
+
let ws = temp_ws().join("restartcrossbindallowfresh");
|
|
1823
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
1824
|
+
std::fs::write(
|
|
1825
|
+
ws.join("TEAM.md"),
|
|
1826
|
+
"---\nname: restartcrossbindallowfresh\nobjective: Restart crossbind repair.\nprovider: codex\n---\n\nteam.\n",
|
|
1827
|
+
)
|
|
1828
|
+
.unwrap();
|
|
1829
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
1830
|
+
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
1831
|
+
let alpha_session = "019f3586-6eb4-7552-8680-c833b483a289";
|
|
1832
|
+
let alpha_rollout = codex_identity_rollout(&ws, "alpha-rollout.jsonl", alpha_session, "alpha");
|
|
1833
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile crossbind team");
|
|
1834
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
1835
|
+
crate::state::persist::save_runtime_state(
|
|
1836
|
+
&ws,
|
|
1837
|
+
&json!({
|
|
1838
|
+
"session_name": "team-restartcrossbindallowfresh",
|
|
1839
|
+
"agents": {
|
|
1840
|
+
"alpha": {
|
|
1841
|
+
"status": "running",
|
|
1842
|
+
"provider": "codex",
|
|
1843
|
+
"window": "alpha",
|
|
1844
|
+
"first_send_at": "2026-07-06T03:45:00+00:00",
|
|
1845
|
+
"session_id": alpha_session,
|
|
1846
|
+
"rollout_path": alpha_rollout.to_string_lossy(),
|
|
1847
|
+
"captured_at": "2026-07-06T03:45:13.596763+00:00",
|
|
1848
|
+
"captured_via": "fs_watch",
|
|
1849
|
+
"attribution_confidence": "high",
|
|
1850
|
+
"spawn_cwd": ws.to_string_lossy()
|
|
1851
|
+
},
|
|
1852
|
+
"bravo": {
|
|
1853
|
+
"status": "running",
|
|
1854
|
+
"provider": "codex",
|
|
1855
|
+
"window": "bravo",
|
|
1856
|
+
"first_send_at": "2026-07-06T03:45:00+00:00",
|
|
1857
|
+
"session_id": alpha_session,
|
|
1858
|
+
"rollout_path": alpha_rollout.to_string_lossy(),
|
|
1859
|
+
"captured_at": "2026-07-06T03:45:13.596763+00:00",
|
|
1860
|
+
"captured_via": "fs_watch",
|
|
1861
|
+
"attribution_confidence": "high",
|
|
1862
|
+
"spawn_cwd": ws.to_string_lossy()
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}),
|
|
1866
|
+
)
|
|
1867
|
+
.unwrap();
|
|
1868
|
+
seed_healthy_coordinator(&ws);
|
|
1869
|
+
let transport = OfflineTransport::new();
|
|
1870
|
+
|
|
1871
|
+
let result = restart_with_transport(&ws, true, None, &transport);
|
|
1872
|
+
|
|
1873
|
+
assert!(
|
|
1874
|
+
matches!(result, Ok(RestartReport::Restarted { .. })),
|
|
1875
|
+
"allow-fresh restart should repair by fresh-spawning only poisoned bravo; got {result:?}"
|
|
1876
|
+
);
|
|
1877
|
+
let windows = transport.spawn_window_records();
|
|
1878
|
+
let records = transport.spawn_records();
|
|
1879
|
+
let bravo_idx = windows
|
|
1880
|
+
.iter()
|
|
1881
|
+
.position(|(_, window)| window == "bravo")
|
|
1882
|
+
.expect("bravo must spawn");
|
|
1883
|
+
assert!(
|
|
1884
|
+
!records[bravo_idx].1.iter().any(|arg| arg == "resume"),
|
|
1885
|
+
"poisoned bravo must fresh-start without codex resume argv; records={records:?}"
|
|
1886
|
+
);
|
|
1887
|
+
|
|
1888
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
1889
|
+
let bravo = state.pointer("/agents/bravo").expect("bravo");
|
|
1890
|
+
for field in [
|
|
1891
|
+
"session_id",
|
|
1892
|
+
"rollout_path",
|
|
1893
|
+
"captured_at",
|
|
1894
|
+
"captured_via",
|
|
1895
|
+
"attribution_confidence",
|
|
1896
|
+
"capture_state",
|
|
1897
|
+
] {
|
|
1898
|
+
let value = bravo.get(field);
|
|
1899
|
+
assert!(
|
|
1900
|
+
value.is_none() || value == Some(&serde_json::Value::Null),
|
|
1901
|
+
"allow-fresh must clear poisoned bravo {field}; got {value:?} in {bravo}"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
if let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) {
|
|
1905
|
+
for (team_key, team_state) in teams {
|
|
1906
|
+
if let Some(team_bravo) = team_state.pointer("/agents/bravo") {
|
|
1907
|
+
for field in [
|
|
1908
|
+
"session_id",
|
|
1909
|
+
"rollout_path",
|
|
1910
|
+
"captured_at",
|
|
1911
|
+
"captured_via",
|
|
1912
|
+
"attribution_confidence",
|
|
1913
|
+
"capture_state",
|
|
1914
|
+
] {
|
|
1915
|
+
let value = team_bravo.get(field);
|
|
1916
|
+
assert!(
|
|
1917
|
+
value.is_none() || value == Some(&serde_json::Value::Null),
|
|
1918
|
+
"allow-fresh must clear teams.{team_key}.agents.bravo {field}; got {value:?} in {team_bravo}"
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
let fresh_session = "019f3587-bravo-fresh";
|
|
1926
|
+
let fresh_rollout =
|
|
1927
|
+
codex_identity_rollout(&ws, "bravo-fresh-rollout.jsonl", fresh_session, "bravo");
|
|
1928
|
+
let mut adapter_for = |provider| crate::provider::adapter::get_adapter(provider);
|
|
1929
|
+
crate::provider::session::capture::capture_missing_provider_sessions_once(
|
|
1930
|
+
&mut state,
|
|
1931
|
+
&mut adapter_for,
|
|
1932
|
+
true,
|
|
1933
|
+
0,
|
|
1934
|
+
)
|
|
1935
|
+
.expect("fresh bravo capture should run");
|
|
1936
|
+
let bravo = state.pointer("/agents/bravo").expect("bravo after capture");
|
|
1937
|
+
assert_eq!(
|
|
1938
|
+
bravo.get("session_id").and_then(serde_json::Value::as_str),
|
|
1939
|
+
Some(fresh_session),
|
|
1940
|
+
"fresh bravo rollout should be capturable after allow-fresh clears poison; rollout={fresh_rollout:?} state={bravo}"
|
|
1941
|
+
);
|
|
1942
|
+
assert_eq!(
|
|
1943
|
+
bravo
|
|
1944
|
+
.get("rollout_path")
|
|
1945
|
+
.and_then(serde_json::Value::as_str),
|
|
1946
|
+
Some(fresh_rollout.to_string_lossy().as_ref())
|
|
1947
|
+
);
|
|
1948
|
+
let plan = crate::lifecycle::restart::classify_restart_plan_with_resume_validation(
|
|
1949
|
+
Some(&ws),
|
|
1950
|
+
&state,
|
|
1951
|
+
false,
|
|
1952
|
+
)
|
|
1953
|
+
.expect("classify after fresh capture");
|
|
1954
|
+
assert!(
|
|
1955
|
+
plan.unresumable.is_empty(),
|
|
1956
|
+
"next restart must not loop on session_identity_mismatch after fresh capture; got {:?}",
|
|
1957
|
+
plan.unresumable
|
|
1958
|
+
);
|
|
1959
|
+
let bravo_decision = plan
|
|
1960
|
+
.decisions
|
|
1961
|
+
.iter()
|
|
1962
|
+
.find(|decision| decision.agent_id.as_str() == "bravo")
|
|
1963
|
+
.expect("bravo decision");
|
|
1964
|
+
assert_eq!(bravo_decision.decision, ResumeDecision::Resume);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1777
1967
|
#[test]
|
|
1778
1968
|
fn restart_spawn_failure_isolated_to_partial_report() {
|
|
1779
1969
|
let ws = restart_ws_two_resumable_workers();
|
|
@@ -2362,10 +2552,7 @@ fn fork_refuses_source_with_partial_tuple() {
|
|
|
2362
2552
|
let src_id = crate::model::ids::AgentId::new("src");
|
|
2363
2553
|
let dst_id = crate::model::ids::AgentId::new("clone");
|
|
2364
2554
|
let result = crate::lifecycle::fork_agent_with_transport(
|
|
2365
|
-
&ws,
|
|
2366
|
-
&src_id,
|
|
2367
|
-
&dst_id,
|
|
2368
|
-
None, // label
|
|
2555
|
+
&ws, &src_id, &dst_id, None, // label
|
|
2369
2556
|
false, // open_display
|
|
2370
2557
|
None, // team
|
|
2371
2558
|
&transport,
|
|
@@ -2433,7 +2620,10 @@ fn add_agent_adaptive_splits_last_non_full_layout_window() {
|
|
|
2433
2620
|
"last non-full layout window should be split"
|
|
2434
2621
|
);
|
|
2435
2622
|
let (_raw, state) = raw_runtime_state(&team);
|
|
2436
|
-
assert_eq!(
|
|
2623
|
+
assert_eq!(
|
|
2624
|
+
state.pointer("/agents/w3/layout_window"),
|
|
2625
|
+
Some(&json!("team-w1"))
|
|
2626
|
+
);
|
|
2437
2627
|
assert_eq!(state.pointer("/agents/w3/pane_index"), Some(&json!(2)));
|
|
2438
2628
|
}
|
|
2439
2629
|
|
|
@@ -2495,7 +2685,10 @@ fn add_agent_adaptive_creates_suffix_window_when_last_layout_full_and_name_colli
|
|
|
2495
2685
|
"full last layout window should create next window with collision suffix"
|
|
2496
2686
|
);
|
|
2497
2687
|
let (_raw, state) = raw_runtime_state(&team);
|
|
2498
|
-
assert_eq!(
|
|
2688
|
+
assert_eq!(
|
|
2689
|
+
state.pointer("/agents/w4/layout_window"),
|
|
2690
|
+
Some(&json!("team-w2-2"))
|
|
2691
|
+
);
|
|
2499
2692
|
assert_eq!(state.pointer("/agents/w4/pane_index"), Some(&json!(0)));
|
|
2500
2693
|
}
|
|
2501
2694
|
|
|
@@ -2508,7 +2701,11 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
|
2508
2701
|
.collect::<Vec<_>>();
|
|
2509
2702
|
let team = quick_start_team_dir_with_roles(&role_refs);
|
|
2510
2703
|
let spec = crate::compiler::compile_team(&team).unwrap();
|
|
2511
|
-
std::fs::write(
|
|
2704
|
+
std::fs::write(
|
|
2705
|
+
team.join("team.spec.yaml"),
|
|
2706
|
+
crate::model::yaml::dumps(&spec),
|
|
2707
|
+
)
|
|
2708
|
+
.unwrap();
|
|
2512
2709
|
let session = "team-layout-stop";
|
|
2513
2710
|
crate::state::persist::save_runtime_state(
|
|
2514
2711
|
&team,
|
|
@@ -2529,8 +2726,7 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
|
2529
2726
|
])
|
|
2530
2727
|
.with_pane_presence("%2", true);
|
|
2531
2728
|
|
|
2532
|
-
let report =
|
|
2533
|
-
stop_agent_with_transport(&team, &AgentId::new("w2"), None, &transport).unwrap();
|
|
2729
|
+
let report = stop_agent_with_transport(&team, &AgentId::new("w2"), None, &transport).unwrap();
|
|
2534
2730
|
|
|
2535
2731
|
assert!(report.stopped);
|
|
2536
2732
|
assert_eq!(report.target, "%2");
|
|
@@ -2544,7 +2740,10 @@ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
|
|
|
2544
2740
|
);
|
|
2545
2741
|
let (_raw, state) = raw_runtime_state(&team);
|
|
2546
2742
|
assert_eq!(state.pointer("/agents/w1/pane_id"), Some(&json!("%1")));
|
|
2547
|
-
assert_eq!(
|
|
2743
|
+
assert_eq!(
|
|
2744
|
+
state.pointer("/agents/w1/layout_window"),
|
|
2745
|
+
Some(&json!("team-w1"))
|
|
2746
|
+
);
|
|
2548
2747
|
}
|
|
2549
2748
|
|
|
2550
2749
|
#[test]
|
|
@@ -2587,7 +2786,10 @@ fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
|
2587
2786
|
"missing worker pane should be rebuilt in its existing layout window"
|
|
2588
2787
|
);
|
|
2589
2788
|
let (_raw, state) = raw_runtime_state(&ws);
|
|
2590
|
-
assert_eq!(
|
|
2789
|
+
assert_eq!(
|
|
2790
|
+
state.pointer("/agents/w1/layout_window"),
|
|
2791
|
+
Some(&json!("team-w1"))
|
|
2792
|
+
);
|
|
2591
2793
|
assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
|
|
2592
2794
|
assert_eq!(state.pointer("/agents/w2/pane_id"), Some(&json!("%2")));
|
|
2593
2795
|
}
|
|
@@ -2848,6 +3050,21 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2848
3050
|
json!("%0"),
|
|
2849
3051
|
"#252 RC2: launch must persist the real pane_id returned by the transport, not drop it when pane_pid is unavailable"
|
|
2850
3052
|
);
|
|
3053
|
+
let events = crate::event_log::EventLog::new(workspace)
|
|
3054
|
+
.tail(0)
|
|
3055
|
+
.expect("read event log");
|
|
3056
|
+
let spawn_event = events
|
|
3057
|
+
.iter()
|
|
3058
|
+
.find(|event| {
|
|
3059
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
3060
|
+
== Some("provider.worker.spawn_argv")
|
|
3061
|
+
&& event.get("agent_id").and_then(serde_json::Value::as_str) == Some("implementer")
|
|
3062
|
+
})
|
|
3063
|
+
.unwrap_or_else(|| panic!("provider.worker.spawn_argv missing; events={events:?}"));
|
|
3064
|
+
assert_eq!(spawn_event["spawn_cwd"], json!(workspace.to_string_lossy()));
|
|
3065
|
+
assert_eq!(spawn_event["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
3066
|
+
assert_eq!(spawn_event["source"], json!("launch"));
|
|
3067
|
+
assert_eq!(spawn_event["spawn_epoch"], json!(0));
|
|
2851
3068
|
}
|
|
2852
3069
|
|
|
2853
3070
|
// Stage B2 — golden launch/core.py:171-173 writes paused workers as exactly
|
|
@@ -2945,12 +3162,12 @@ fn add_agent_resolves_to_persisted_endpoint_socket_not_workspace_hash() {
|
|
|
2945
3162
|
)
|
|
2946
3163
|
.unwrap();
|
|
2947
3164
|
|
|
2948
|
-
let resolved =
|
|
2949
|
-
&team, None
|
|
2950
|
-
|
|
2951
|
-
.expect("resolver must succeed when state is present");
|
|
3165
|
+
let resolved =
|
|
3166
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(&team, None)
|
|
3167
|
+
.expect("resolver must succeed when state is present");
|
|
2952
3168
|
|
|
2953
|
-
let endpoint =
|
|
3169
|
+
let endpoint =
|
|
3170
|
+
<crate::tmux_backend::TmuxBackend as crate::transport::Transport>::tmux_endpoint(&resolved);
|
|
2954
3171
|
assert_eq!(
|
|
2955
3172
|
endpoint.as_deref(),
|
|
2956
3173
|
Some(live_endpoint.as_str()),
|
|
@@ -2973,12 +3190,12 @@ fn add_agent_resolver_falls_back_to_workspace_socket_when_no_persisted_endpoint(
|
|
|
2973
3190
|
std::fs::create_dir_all(&team).unwrap();
|
|
2974
3191
|
// NOTE: no save_runtime_state — cold workspace, no persisted endpoint.
|
|
2975
3192
|
|
|
2976
|
-
let resolved =
|
|
2977
|
-
&team, None
|
|
2978
|
-
|
|
2979
|
-
.expect("resolver must succeed (fall back to for_workspace) on cold workspace");
|
|
3193
|
+
let resolved =
|
|
3194
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(&team, None)
|
|
3195
|
+
.expect("resolver must succeed (fall back to for_workspace) on cold workspace");
|
|
2980
3196
|
|
|
2981
|
-
let endpoint =
|
|
3197
|
+
let endpoint =
|
|
3198
|
+
<crate::tmux_backend::TmuxBackend as crate::transport::Transport>::tmux_endpoint(&resolved);
|
|
2982
3199
|
let fallback = <crate::tmux_backend::TmuxBackend as crate::transport::Transport>::tmux_endpoint(
|
|
2983
3200
|
&crate::tmux_backend::TmuxBackend::for_workspace(&team),
|
|
2984
3201
|
);
|
|
@@ -3044,7 +3261,9 @@ fn add_agent_reachability_gate_returns_error_when_spawn_pane_not_addressable() {
|
|
|
3044
3261
|
);
|
|
3045
3262
|
let err_msg = result.unwrap_err().to_string();
|
|
3046
3263
|
assert!(
|
|
3047
|
-
err_msg.contains("unreachable")
|
|
3264
|
+
err_msg.contains("unreachable")
|
|
3265
|
+
|| err_msg.contains("not addressable")
|
|
3266
|
+
|| err_msg.contains("socket drift"),
|
|
3048
3267
|
"0.3.24 reachability gate: error message must name the unreachable / \
|
|
3049
3268
|
socket-drift symptom so operators can diagnose. Got: {err_msg}"
|
|
3050
3269
|
);
|
|
@@ -3701,16 +3920,31 @@ fn e45_is_adaptive_layout_window_recognises_team_w_pattern_only() {
|
|
|
3701
3920
|
assert!(is_adapt("team-w1"), "team-w1 must be adaptive");
|
|
3702
3921
|
assert!(is_adapt("team-w2"), "team-w2 must be adaptive");
|
|
3703
3922
|
assert!(is_adapt("team-w42"), "team-w42 must be adaptive");
|
|
3704
|
-
assert!(
|
|
3923
|
+
assert!(
|
|
3924
|
+
is_adapt("team-w7-suffix"),
|
|
3925
|
+
"team-w7-suffix must be adaptive"
|
|
3926
|
+
);
|
|
3705
3927
|
|
|
3706
|
-
assert!(
|
|
3707
|
-
|
|
3928
|
+
assert!(
|
|
3929
|
+
!is_adapt("developer"),
|
|
3930
|
+
"developer is per-agent, not adaptive"
|
|
3931
|
+
);
|
|
3932
|
+
assert!(
|
|
3933
|
+
!is_adapt("architect"),
|
|
3934
|
+
"architect is per-agent, not adaptive"
|
|
3935
|
+
);
|
|
3708
3936
|
assert!(!is_adapt("demo-director"), "demo-director is per-agent");
|
|
3709
3937
|
assert!(!is_adapt("leader"), "leader is not adaptive");
|
|
3710
3938
|
assert!(!is_adapt(""), "empty is not adaptive");
|
|
3711
3939
|
assert!(!is_adapt("team-w"), "team-w (no index) is not adaptive");
|
|
3712
|
-
assert!(
|
|
3713
|
-
|
|
3940
|
+
assert!(
|
|
3941
|
+
!is_adapt("team-w0"),
|
|
3942
|
+
"team-w0 (index 0 invalid post-subtract) is not adaptive"
|
|
3943
|
+
);
|
|
3944
|
+
assert!(
|
|
3945
|
+
!is_adapt("team-wfoo"),
|
|
3946
|
+
"team-wfoo (non-numeric) is not adaptive"
|
|
3947
|
+
);
|
|
3714
3948
|
}
|
|
3715
3949
|
|
|
3716
3950
|
// ═════════════════════════════════════════════════════════════════════════════
|