@team-agent/installer 0.5.51 → 0.5.53
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/named_address.rs +35 -67
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +526 -73
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +20 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
|
@@ -8,6 +8,60 @@ const DELEG_ROLE_ALPHA_COMPAT: &str = "---\nname: alpha\nrole: Alpha Worker\npro
|
|
|
8
8
|
type LaneKills = std::sync::Arc<std::sync::Mutex<Vec<String>>>;
|
|
9
9
|
pub(super) type LaneSpawns = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<String>)>>>;
|
|
10
10
|
|
|
11
|
+
fn emit_codex_fork_backing(argv: &[String], cwd: &std::path::Path) {
|
|
12
|
+
if !argv.windows(2).any(|pair| pair == ["codex", "fork"]) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
static SESSION_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
16
|
+
let sequence = SESSION_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
17
|
+
let session_id = format!("lane-fork-{}-{sequence}", std::process::id());
|
|
18
|
+
let root = std::env::var_os("HOME")
|
|
19
|
+
.map(std::path::PathBuf::from)
|
|
20
|
+
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
|
21
|
+
.join(".codex/sessions/lane-fixture");
|
|
22
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
23
|
+
std::fs::write(
|
|
24
|
+
root.join(format!("rollout-{session_id}.jsonl")),
|
|
25
|
+
format!(
|
|
26
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}},\"created_at\":\"{}\"}}\n",
|
|
27
|
+
cwd.to_string_lossy(),
|
|
28
|
+
chrono::Utc::now().to_rfc3339()
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
.unwrap();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(super) struct LaneHomeGuard {
|
|
35
|
+
previous: Option<std::ffi::OsString>,
|
|
36
|
+
root: PathBuf,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl LaneHomeGuard {
|
|
40
|
+
pub(super) fn enter(tag: &str) -> Self {
|
|
41
|
+
static HOME_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
42
|
+
let root = std::env::temp_dir().join(format!(
|
|
43
|
+
"ta-lane-home-{tag}-{}-{}",
|
|
44
|
+
std::process::id(),
|
|
45
|
+
HOME_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
|
46
|
+
));
|
|
47
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
48
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
49
|
+
let previous = std::env::var_os("HOME");
|
|
50
|
+
std::env::set_var("HOME", &root);
|
|
51
|
+
Self { previous, root }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl Drop for LaneHomeGuard {
|
|
56
|
+
fn drop(&mut self) {
|
|
57
|
+
match &self.previous {
|
|
58
|
+
Some(value) => std::env::set_var("HOME", value),
|
|
59
|
+
None => std::env::remove_var("HOME"),
|
|
60
|
+
}
|
|
61
|
+
let _ = std::fs::remove_dir_all(&self.root);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
11
65
|
/// Recording transport for Lane-A v2: `list_windows`/`list_targets` answer from a configurable window
|
|
12
66
|
/// set (golden's `_tmux_window_exists` primitive = `tmux list-windows`); `kill_window` + spawn_first/into
|
|
13
67
|
/// are RECORDED. Every other method returns a benign Ok (never panics) so stop/reset/remove/fork run
|
|
@@ -18,6 +72,7 @@ pub(super) struct LaneTransport {
|
|
|
18
72
|
foreign: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>>,
|
|
19
73
|
killed: LaneKills,
|
|
20
74
|
spawns: LaneSpawns,
|
|
75
|
+
spawn_observed_at: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
|
|
21
76
|
}
|
|
22
77
|
impl LaneTransport {
|
|
23
78
|
pub(super) fn new(session: &str, windows: &[&str]) -> Self {
|
|
@@ -29,6 +84,7 @@ impl LaneTransport {
|
|
|
29
84
|
foreign: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
30
85
|
killed: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
31
86
|
spawns: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
87
|
+
spawn_observed_at: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
32
88
|
}
|
|
33
89
|
}
|
|
34
90
|
pub(super) fn killed(&self) -> Vec<String> {
|
|
@@ -37,6 +93,9 @@ impl LaneTransport {
|
|
|
37
93
|
pub(super) fn spawns(&self) -> Vec<(String, Vec<String>)> {
|
|
38
94
|
self.spawns.lock().unwrap().clone()
|
|
39
95
|
}
|
|
96
|
+
fn spawn_observed_at(&self) -> Vec<String> {
|
|
97
|
+
self.spawn_observed_at.lock().unwrap().clone()
|
|
98
|
+
}
|
|
40
99
|
fn add_foreign(&self, session: &str, window: &str) {
|
|
41
100
|
self.foreign
|
|
42
101
|
.lock()
|
|
@@ -53,9 +112,14 @@ impl crate::transport::Transport for LaneTransport {
|
|
|
53
112
|
session: &crate::transport::SessionName,
|
|
54
113
|
window: &crate::transport::WindowName,
|
|
55
114
|
argv: &[String],
|
|
56
|
-
|
|
115
|
+
c: &std::path::Path,
|
|
57
116
|
_e: &std::collections::BTreeMap<String, String>,
|
|
58
117
|
) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
118
|
+
self.spawn_observed_at
|
|
119
|
+
.lock()
|
|
120
|
+
.unwrap()
|
|
121
|
+
.push(chrono::Utc::now().to_rfc3339());
|
|
122
|
+
emit_codex_fork_backing(argv, c);
|
|
59
123
|
self.spawns
|
|
60
124
|
.lock()
|
|
61
125
|
.unwrap()
|
|
@@ -84,9 +148,14 @@ impl crate::transport::Transport for LaneTransport {
|
|
|
84
148
|
session: &crate::transport::SessionName,
|
|
85
149
|
window: &crate::transport::WindowName,
|
|
86
150
|
argv: &[String],
|
|
87
|
-
|
|
151
|
+
c: &std::path::Path,
|
|
88
152
|
_e: &std::collections::BTreeMap<String, String>,
|
|
89
153
|
) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
154
|
+
self.spawn_observed_at
|
|
155
|
+
.lock()
|
|
156
|
+
.unwrap()
|
|
157
|
+
.push(chrono::Utc::now().to_rfc3339());
|
|
158
|
+
emit_codex_fork_backing(argv, c);
|
|
90
159
|
self.spawns
|
|
91
160
|
.lock()
|
|
92
161
|
.unwrap()
|
|
@@ -147,7 +216,7 @@ impl crate::transport::Transport for LaneTransport {
|
|
|
147
216
|
&self,
|
|
148
217
|
_p: &crate::transport::PaneId,
|
|
149
218
|
) -> Result<crate::model::enums::PaneLiveness, crate::transport::TransportError> {
|
|
150
|
-
Ok(crate::model::enums::PaneLiveness::
|
|
219
|
+
Ok(crate::model::enums::PaneLiveness::Live)
|
|
151
220
|
}
|
|
152
221
|
fn list_targets(
|
|
153
222
|
&self,
|
|
@@ -434,6 +503,319 @@ fn force_remove_reconciles_all_seat_quadrants_without_cross_team_same_name_kill(
|
|
|
434
503
|
}
|
|
435
504
|
}
|
|
436
505
|
|
|
506
|
+
#[test]
|
|
507
|
+
fn remove_persists_retired_tombstone_and_explicit_readd_clears_it() {
|
|
508
|
+
let ws = lanea_ws_agents(json!({
|
|
509
|
+
"alpha": { "status": "stopped", "provider": "codex", "window": "alpha" },
|
|
510
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
511
|
+
}));
|
|
512
|
+
let mut seeded = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
513
|
+
seeded["team_key"] = json!("laneateam");
|
|
514
|
+
seeded["active_team_key"] = json!("laneateam");
|
|
515
|
+
let mut nested = crate::state::projection::compact_team_state(&seeded);
|
|
516
|
+
nested["status"] = json!("alive");
|
|
517
|
+
seeded["teams"] = json!({
|
|
518
|
+
"laneateam": nested,
|
|
519
|
+
"sibling": {
|
|
520
|
+
"team_key": "sibling",
|
|
521
|
+
"status": "shutdown",
|
|
522
|
+
"agents": {},
|
|
523
|
+
"agent_lifecycle": {
|
|
524
|
+
"alpha": {
|
|
525
|
+
"state": "retired",
|
|
526
|
+
"changed_at": "2026-07-20T00:00:00Z",
|
|
527
|
+
"reason": "sibling retirement"
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
crate::state::persist::save_runtime_state(&ws, &seeded).unwrap();
|
|
533
|
+
let stale_tick = crate::state::projection::select_runtime_state(&ws, Some("laneateam"))
|
|
534
|
+
.expect("capture the coordinator projection from before remove");
|
|
535
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
536
|
+
let role = ws.join("agents").join("alpha.md");
|
|
537
|
+
|
|
538
|
+
remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx).unwrap();
|
|
539
|
+
let removed: serde_json::Value = serde_json::from_str(
|
|
540
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
541
|
+
)
|
|
542
|
+
.unwrap();
|
|
543
|
+
assert_eq!(
|
|
544
|
+
removed["teams"]["laneateam"]["agent_lifecycle"]["alpha"]["state"], "retired",
|
|
545
|
+
"the first successful remove must directly persist the canonical tombstone"
|
|
546
|
+
);
|
|
547
|
+
assert!(
|
|
548
|
+
removed["teams"]["laneateam"]["agents"]
|
|
549
|
+
.get("alpha")
|
|
550
|
+
.is_none(),
|
|
551
|
+
"the first successful remove must directly delete the canonical agent row"
|
|
552
|
+
);
|
|
553
|
+
crate::state::repository::StateRepository::new(&ws)
|
|
554
|
+
.save(
|
|
555
|
+
crate::state::repository::StateWriteIntent::CoordinatorTick {
|
|
556
|
+
team_key: "laneateam",
|
|
557
|
+
},
|
|
558
|
+
&stale_tick,
|
|
559
|
+
)
|
|
560
|
+
.unwrap();
|
|
561
|
+
let after_stale_tick: serde_json::Value = serde_json::from_str(
|
|
562
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
563
|
+
)
|
|
564
|
+
.unwrap();
|
|
565
|
+
assert_eq!(
|
|
566
|
+
after_stale_tick["teams"]["laneateam"]["agent_lifecycle"]["alpha"]["state"], "retired",
|
|
567
|
+
"a stale coordinator save must not erase the first remove's tombstone"
|
|
568
|
+
);
|
|
569
|
+
assert!(
|
|
570
|
+
after_stale_tick["teams"]["laneateam"]["agents"]
|
|
571
|
+
.get("alpha")
|
|
572
|
+
.is_none(),
|
|
573
|
+
"a stale coordinator save must not resurrect the first removed agent"
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
crate::lifecycle::add_agent_with_transport(&ws, &aid("alpha"), &role, false, None, &tx)
|
|
577
|
+
.unwrap();
|
|
578
|
+
let readded = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
579
|
+
assert!(
|
|
580
|
+
readded["teams"]["laneateam"]["agent_lifecycle"]
|
|
581
|
+
.get("alpha")
|
|
582
|
+
.is_none(),
|
|
583
|
+
"explicit re-add must consume the retired tombstone"
|
|
584
|
+
);
|
|
585
|
+
assert!(readded["teams"]["laneateam"]["agents"]
|
|
586
|
+
.get("alpha")
|
|
587
|
+
.is_some());
|
|
588
|
+
assert_eq!(
|
|
589
|
+
readded["teams"]["sibling"]["agent_lifecycle"]["alpha"]["state"], "retired",
|
|
590
|
+
"selected-team re-add must not clear a sibling team's same-id retirement"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/// Cross-product RED (verifier, slice2b authority leak): the FAILURE dimension
|
|
595
|
+
/// (a stale writer whose incoming projection predates a sibling retirement) x the
|
|
596
|
+
/// BOUNDARY dimension (a sibling team carrying the SAME agent id). The tracked
|
|
597
|
+
/// `remove_persists_..._readd_clears_it` sibling assertion covers only the
|
|
598
|
+
/// non-stale corner (sibling tombstone already current in the add's input); it
|
|
599
|
+
/// does NOT exercise an incoming sibling projection captured BEFORE the sibling
|
|
600
|
+
/// retirement. topology-authority is keyed by bare `agent_id` with no team
|
|
601
|
+
/// dimension (repository.rs:277-283 drops team_key; persist.rs iterates every
|
|
602
|
+
/// sibling team with the same global id set), so an explicit AddAgent(alpha) in
|
|
603
|
+
/// team A exempts alpha in team B too and a stale incoming resurrects B's alpha /
|
|
604
|
+
/// erases B's retirement tombstone. Red at da11f98c: teamB.alpha lifecycle Null.
|
|
605
|
+
#[test]
|
|
606
|
+
fn add_agent_authority_must_not_cross_team_erase_sibling_same_id_retirement() {
|
|
607
|
+
// Disk latest: team A alive with alpha; team B alpha ABSENT + retired.
|
|
608
|
+
let ws = lanea_ws_agents(json!({
|
|
609
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha" },
|
|
610
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
611
|
+
}));
|
|
612
|
+
let mut latest = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
613
|
+
latest["team_key"] = json!("laneateam");
|
|
614
|
+
latest["active_team_key"] = json!("laneateam");
|
|
615
|
+
let mut nested_a = crate::state::projection::compact_team_state(&latest);
|
|
616
|
+
nested_a["status"] = json!("alive");
|
|
617
|
+
latest["teams"] = json!({
|
|
618
|
+
"laneateam": nested_a,
|
|
619
|
+
"sibling": {
|
|
620
|
+
"team_key": "sibling",
|
|
621
|
+
"status": "alive",
|
|
622
|
+
"agents": {},
|
|
623
|
+
"agent_lifecycle": {
|
|
624
|
+
"alpha": {
|
|
625
|
+
"state": "retired",
|
|
626
|
+
"changed_at": "2026-07-21T00:00:00Z",
|
|
627
|
+
"reason": "sibling retirement (newer than the stale incoming)"
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
crate::state::persist::save_runtime_state(&ws, &latest).unwrap();
|
|
633
|
+
|
|
634
|
+
// Incoming STALE root: captured BEFORE team B retired alpha, so its sibling
|
|
635
|
+
// entry still carries alpha ACTIVE with no tombstone. A concurrent explicit
|
|
636
|
+
// AddAgent(alpha) in team A carries this whole stale multi-team projection.
|
|
637
|
+
let mut incoming_stale = latest.clone();
|
|
638
|
+
incoming_stale["teams"]["sibling"] = json!({
|
|
639
|
+
"team_key": "sibling",
|
|
640
|
+
"status": "alive",
|
|
641
|
+
"agents": { "alpha": { "status": "running", "provider": "codex", "window": "alpha" } },
|
|
642
|
+
"agent_lifecycle": {}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
crate::state::repository::StateRepository::new(&ws)
|
|
646
|
+
.save(
|
|
647
|
+
crate::state::repository::StateWriteIntent::AddAgent {
|
|
648
|
+
team_key: "laneateam",
|
|
649
|
+
agent_id: "alpha",
|
|
650
|
+
},
|
|
651
|
+
&incoming_stale,
|
|
652
|
+
)
|
|
653
|
+
.unwrap();
|
|
654
|
+
|
|
655
|
+
let after: serde_json::Value = serde_json::from_str(
|
|
656
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
657
|
+
)
|
|
658
|
+
.unwrap();
|
|
659
|
+
// The AddAgent authority is scoped to team A's alpha ONLY; team B's newer
|
|
660
|
+
// same-id retirement must survive (the stale incoming must not resurrect it).
|
|
661
|
+
assert_eq!(
|
|
662
|
+
after["teams"]["sibling"]["agent_lifecycle"]["alpha"]["state"],
|
|
663
|
+
json!("retired"),
|
|
664
|
+
"selected-team AddAgent authority must not cross team boundary by bare agent id: \
|
|
665
|
+
team B's newer same-id retirement was erased by team A's stale re-add"
|
|
666
|
+
);
|
|
667
|
+
assert!(
|
|
668
|
+
after["teams"]["sibling"]["agents"].get("alpha").is_none(),
|
|
669
|
+
"a stale incoming must not resurrect the sibling team's retired same-id agent"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/// Identity-source boundary (verifier, slice2b authority fix new adjudication
|
|
674
|
+
/// face, same family as slice1 empty-current): the legacy top-level scope anchor
|
|
675
|
+
/// now prefers `active_team_key`. Verify it does not over-exempt / under-protect
|
|
676
|
+
/// when `active_team_key` is a NON-EMPTY but TERMINAL / mismatched value (points
|
|
677
|
+
/// at a team not present in `teams`). The AddAgent authority is (laneateam,
|
|
678
|
+
/// alpha); with a ghost active_team_key, the top-level filter must resolve NO
|
|
679
|
+
/// exemption for the ghost, so a real retirement anywhere is still protected —
|
|
680
|
+
/// the anchor must not silently widen authority to a bare id under a dangling
|
|
681
|
+
/// active_team_key (that would be the empty-current forgery shape at slice1).
|
|
682
|
+
#[test]
|
|
683
|
+
fn terminal_active_team_key_does_not_widen_topology_authority_to_bare_id() {
|
|
684
|
+
// Disk latest: laneateam alive with alpha; sibling alpha ABSENT + retired.
|
|
685
|
+
let ws = lanea_ws_agents(json!({
|
|
686
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha" },
|
|
687
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
688
|
+
}));
|
|
689
|
+
let mut latest = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
690
|
+
latest["team_key"] = json!("laneateam");
|
|
691
|
+
latest["active_team_key"] = json!("laneateam");
|
|
692
|
+
let mut nested_a = crate::state::projection::compact_team_state(&latest);
|
|
693
|
+
nested_a["status"] = json!("alive");
|
|
694
|
+
latest["teams"] = json!({
|
|
695
|
+
"laneateam": nested_a,
|
|
696
|
+
"sibling": {
|
|
697
|
+
"team_key": "sibling",
|
|
698
|
+
"status": "alive",
|
|
699
|
+
"agents": {},
|
|
700
|
+
"agent_lifecycle": {
|
|
701
|
+
"alpha": { "state": "retired", "changed_at": "2026-07-21T00:00:00Z", "reason": "sibling" }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
crate::state::persist::save_runtime_state(&ws, &latest).unwrap();
|
|
706
|
+
|
|
707
|
+
// Incoming STALE root whose active_team_key points at a GHOST team not in
|
|
708
|
+
// `teams`, and whose sibling entry is the pre-retire (alpha active) shape.
|
|
709
|
+
let mut incoming_stale = latest.clone();
|
|
710
|
+
incoming_stale["active_team_key"] = json!("ghostteam-does-not-exist");
|
|
711
|
+
incoming_stale["teams"]["sibling"] = json!({
|
|
712
|
+
"team_key": "sibling",
|
|
713
|
+
"status": "alive",
|
|
714
|
+
"agents": { "alpha": { "status": "running", "provider": "codex", "window": "alpha" } },
|
|
715
|
+
"agent_lifecycle": {}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
crate::state::repository::StateRepository::new(&ws)
|
|
719
|
+
.save(
|
|
720
|
+
crate::state::repository::StateWriteIntent::AddAgent {
|
|
721
|
+
team_key: "laneateam",
|
|
722
|
+
agent_id: "alpha",
|
|
723
|
+
},
|
|
724
|
+
&incoming_stale,
|
|
725
|
+
)
|
|
726
|
+
.unwrap();
|
|
727
|
+
|
|
728
|
+
let after: serde_json::Value = serde_json::from_str(
|
|
729
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
730
|
+
)
|
|
731
|
+
.unwrap();
|
|
732
|
+
// A dangling active_team_key must not widen the (laneateam, alpha) authority
|
|
733
|
+
// into a bare `alpha` exemption that reaches the sibling: sibling stays
|
|
734
|
+
// retired, not resurrected.
|
|
735
|
+
assert_eq!(
|
|
736
|
+
after["teams"]["sibling"]["agent_lifecycle"]["alpha"]["state"],
|
|
737
|
+
json!("retired"),
|
|
738
|
+
"a terminal/ghost active_team_key must not widen topology authority to a bare id \
|
|
739
|
+
and erase the sibling's same-id retirement"
|
|
740
|
+
);
|
|
741
|
+
assert!(
|
|
742
|
+
after["teams"]["sibling"]["agents"].get("alpha").is_none(),
|
|
743
|
+
"a terminal active_team_key must not let a stale incoming resurrect the sibling agent"
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// Cross-product cell (verifier, slice2b current-alias fix): the `current`
|
|
748
|
+
/// physical alias dimension x the sibling same-id retirement dimension. The
|
|
749
|
+
/// reset fix routes `team == "current"` authority through `top_level_team`
|
|
750
|
+
/// (active_team_key). This pins that the alias branch does NOT leak that
|
|
751
|
+
/// active-team authority into a REAL sibling that merely shares the agent id:
|
|
752
|
+
/// a `current` alias entry (= the active team's physical projection) plus a
|
|
753
|
+
/// distinct sibling team both carrying `alpha`, sibling retired, stale incoming
|
|
754
|
+
/// resurrecting sibling alpha. The current-alias authority must clear only the
|
|
755
|
+
/// active team's own alpha, never the sibling's newer retirement.
|
|
756
|
+
#[test]
|
|
757
|
+
fn current_alias_authority_does_not_leak_into_real_sibling_same_id_retirement() {
|
|
758
|
+
let ws = lanea_ws_agents(json!({
|
|
759
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha" },
|
|
760
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
761
|
+
}));
|
|
762
|
+
let mut latest = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
763
|
+
latest["team_key"] = json!("laneateam");
|
|
764
|
+
latest["active_team_key"] = json!("laneateam");
|
|
765
|
+
let mut nested_a = crate::state::projection::compact_team_state(&latest);
|
|
766
|
+
nested_a["status"] = json!("alive");
|
|
767
|
+
// `current` is the physical alias of the active team (laneateam); a REAL
|
|
768
|
+
// sibling team also carries alpha, retired and newer than the stale incoming.
|
|
769
|
+
latest["teams"] = json!({
|
|
770
|
+
"laneateam": nested_a.clone(),
|
|
771
|
+
"current": nested_a,
|
|
772
|
+
"sibling": {
|
|
773
|
+
"team_key": "sibling",
|
|
774
|
+
"status": "alive",
|
|
775
|
+
"agents": {},
|
|
776
|
+
"agent_lifecycle": {
|
|
777
|
+
"alpha": { "state": "retired", "changed_at": "2026-07-21T00:00:00Z", "reason": "sibling" }
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
crate::state::persist::save_runtime_state(&ws, &latest).unwrap();
|
|
782
|
+
|
|
783
|
+
let mut incoming_stale = latest.clone();
|
|
784
|
+
incoming_stale["teams"]["sibling"] = json!({
|
|
785
|
+
"team_key": "sibling",
|
|
786
|
+
"status": "alive",
|
|
787
|
+
"agents": { "alpha": { "status": "running", "provider": "codex", "window": "alpha" } },
|
|
788
|
+
"agent_lifecycle": {}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
crate::state::repository::StateRepository::new(&ws)
|
|
792
|
+
.save(
|
|
793
|
+
crate::state::repository::StateWriteIntent::AddAgent {
|
|
794
|
+
team_key: "laneateam",
|
|
795
|
+
agent_id: "alpha",
|
|
796
|
+
},
|
|
797
|
+
&incoming_stale,
|
|
798
|
+
)
|
|
799
|
+
.unwrap();
|
|
800
|
+
|
|
801
|
+
let after: serde_json::Value = serde_json::from_str(
|
|
802
|
+
&std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
|
|
803
|
+
)
|
|
804
|
+
.unwrap();
|
|
805
|
+
// The current-alias authority is the ACTIVE team's; it must not reach the
|
|
806
|
+
// real sibling that only shares the id.
|
|
807
|
+
assert_eq!(
|
|
808
|
+
after["teams"]["sibling"]["agent_lifecycle"]["alpha"]["state"],
|
|
809
|
+
json!("retired"),
|
|
810
|
+
"current-alias authority must not leak into a real sibling's same-id retirement"
|
|
811
|
+
);
|
|
812
|
+
assert!(
|
|
813
|
+
after["teams"]["sibling"]["agents"].get("alpha").is_none(),
|
|
814
|
+
"a stale incoming must not resurrect the real sibling's retired same-id agent \
|
|
815
|
+
via the current-alias authority path"
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
437
819
|
const CHARLIE_ROLE: &str = "---\nname: charlie\nrole: Charlie Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCharlie.\n";
|
|
438
820
|
|
|
439
821
|
struct HermeticTestEnv {
|
|
@@ -558,6 +940,10 @@ fn force_recreate_is_one_lock_transaction_for_all_seat_quadrants_and_rolls_back_
|
|
|
558
940
|
state["agents"].get("charlie").is_some(),
|
|
559
941
|
"{quadrant}: state restored"
|
|
560
942
|
);
|
|
943
|
+
assert!(
|
|
944
|
+
state["agent_lifecycle"].get("charlie").is_none(),
|
|
945
|
+
"{quadrant}: successful force-recreate consumes its interim tombstone"
|
|
946
|
+
);
|
|
561
947
|
let spec_text = std::fs::read_to_string(selected_spec_path(&ws)).unwrap();
|
|
562
948
|
assert_eq!(
|
|
563
949
|
spec_text.matches("id: \"charlie\"").count(),
|
|
@@ -604,6 +990,10 @@ fn force_recreate_is_one_lock_transaction_for_all_seat_quadrants_and_rolls_back_
|
|
|
604
990
|
state["agents"].get("charlie").is_some(),
|
|
605
991
|
"rollback restores state"
|
|
606
992
|
);
|
|
993
|
+
assert!(
|
|
994
|
+
state["agent_lifecycle"].get("charlie").is_none(),
|
|
995
|
+
"preflight failure must not create a retirement tombstone"
|
|
996
|
+
);
|
|
607
997
|
assert!(
|
|
608
998
|
std::fs::read_to_string(selected_spec_path(&ws))
|
|
609
999
|
.unwrap()
|
|
@@ -702,6 +1092,11 @@ fn force_recreate_post_consumption_failure_restores_before_tuple_and_health() {
|
|
|
702
1092
|
sibling_health,
|
|
703
1093
|
"rollback never changes the sibling team's same-agent health row"
|
|
704
1094
|
);
|
|
1095
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1096
|
+
assert!(
|
|
1097
|
+
state["agent_lifecycle"].get("charlie").is_none(),
|
|
1098
|
+
"post-consumption rollback restores the before-state without a new tombstone"
|
|
1099
|
+
);
|
|
705
1100
|
}
|
|
706
1101
|
|
|
707
1102
|
#[test]
|
|
@@ -1140,6 +1535,11 @@ fn lanea_remove_rollback_restarts_force_stopped_worker() {
|
|
|
1140
1535
|
RemoveRollback::restore. spawns={:?}",
|
|
1141
1536
|
tx.spawns()
|
|
1142
1537
|
);
|
|
1538
|
+
let restored = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1539
|
+
assert!(
|
|
1540
|
+
restored["agent_lifecycle"].get("alpha").is_none(),
|
|
1541
|
+
"failed remove must roll back the transaction-created tombstone"
|
|
1542
|
+
);
|
|
1143
1543
|
}
|
|
1144
1544
|
|
|
1145
1545
|
// ── REMOVE #11 (remove-dynamic-role-path-and-required-8, warn) [RED] — missing REQUIRED role file raises
|
|
@@ -1263,14 +1663,11 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
|
1263
1663
|
);
|
|
1264
1664
|
}
|
|
1265
1665
|
|
|
1266
|
-
//
|
|
1267
|
-
// Golden operations.py:399,408 returns state['agents'][as_agent_id].get('session_id') — the captured
|
|
1268
|
-
// provider session id (or None if capture missed, raise_on_missed=False). Rust sets
|
|
1269
|
-
// session_id: Some(SessionId::new(spawn.pane_id)) (launch.rs:502) — the tmux pane id ('%newfork'),
|
|
1270
|
-
// a different value kind. RED: the report session_id must NOT be the pane id (None, since the Rust fork
|
|
1271
|
-
// path performs no session capture).
|
|
1666
|
+
// Successful fork reports the verified provider session id, never the pane id.
|
|
1272
1667
|
#[test]
|
|
1668
|
+
#[serial_test::serial(env)]
|
|
1273
1669
|
fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
1670
|
+
let _home = LaneHomeGuard::enter("fork-report");
|
|
1274
1671
|
let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
|
|
1275
1672
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
1276
1673
|
let report =
|
|
@@ -1282,9 +1679,41 @@ fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
|
1282
1679
|
"golden operations.py:399,408: report.session_id is the captured provider session id / None, NEVER the \
|
|
1283
1680
|
tmux pane id; Rust returns Some(pane_id='%newfork')"
|
|
1284
1681
|
);
|
|
1285
|
-
|
|
1286
|
-
report.session_id,
|
|
1287
|
-
"
|
|
1682
|
+
assert!(
|
|
1683
|
+
report.session_id.is_some(),
|
|
1684
|
+
"a successful fork must carry the verified NEW provider session id"
|
|
1685
|
+
);
|
|
1686
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1687
|
+
let row = &state["agents"]["newfork"];
|
|
1688
|
+
assert!(row["spawned_at"]
|
|
1689
|
+
.as_str()
|
|
1690
|
+
.is_some_and(|value| chrono::DateTime::parse_from_rfc3339(value).is_ok()));
|
|
1691
|
+
assert_eq!(row["spawn_epoch"], serde_json::json!(1));
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
#[test]
|
|
1695
|
+
fn fresh_start_persists_boundary_from_before_transport_spawn() {
|
|
1696
|
+
let ws = lanea_one_agent_ws("stopped");
|
|
1697
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
1698
|
+
crate::lifecycle::start_agent_with_transport(&ws, &aid("alpha"), false, false, true, None, &tx)
|
|
1699
|
+
.expect("fresh start");
|
|
1700
|
+
|
|
1701
|
+
let transport_observed_at = tx
|
|
1702
|
+
.spawn_observed_at()
|
|
1703
|
+
.into_iter()
|
|
1704
|
+
.next()
|
|
1705
|
+
.expect("transport spawn observation");
|
|
1706
|
+
let transport_observed_at =
|
|
1707
|
+
chrono::DateTime::parse_from_rfc3339(&transport_observed_at).unwrap();
|
|
1708
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1709
|
+
let spawned_at = state["agents"]["alpha"]["spawned_at"]
|
|
1710
|
+
.as_str()
|
|
1711
|
+
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
|
|
1712
|
+
.expect("persisted spawn boundary");
|
|
1713
|
+
assert!(
|
|
1714
|
+
spawned_at <= transport_observed_at,
|
|
1715
|
+
"the persisted capture boundary must precede the transport spawn; \
|
|
1716
|
+
spawned_at={spawned_at}, transport_observed_at={transport_observed_at}"
|
|
1288
1717
|
);
|
|
1289
1718
|
}
|
|
1290
1719
|
|
|
@@ -71,10 +71,11 @@ fn codex_identity_rollout(
|
|
|
71
71
|
std::fs::write(
|
|
72
72
|
&path,
|
|
73
73
|
format!(
|
|
74
|
-
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
|
|
74
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\",\"created_at\":\"{}\"}}}}}}\n\
|
|
75
75
|
{{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
|
|
76
76
|
{{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
|
|
77
|
-
workspace.to_string_lossy()
|
|
77
|
+
workspace.to_string_lossy(),
|
|
78
|
+
chrono::Utc::now().to_rfc3339()
|
|
78
79
|
),
|
|
79
80
|
)
|
|
80
81
|
.unwrap();
|
|
@@ -705,6 +706,7 @@ fn spine_add_agent_rejects_duplicate_agent_id() {
|
|
|
705
706
|
#[test]
|
|
706
707
|
fn e5_add_agent_does_not_copy_role_into_platform_dir_and_injects_into_spec() {
|
|
707
708
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
709
|
+
seed_canonical_team_identity(&team, "teamdir");
|
|
708
710
|
// External role file OUTSIDE team_dir/agents, with recognizable body content.
|
|
709
711
|
let role = team.parent().unwrap().join("w2-external-role.md");
|
|
710
712
|
let role_body = "---\nname: w2\nrole: Second Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nUNIQUE-E5-BUG1-MARKER body for w2.\n";
|
|
@@ -1154,6 +1156,12 @@ pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
|
|
|
1154
1156
|
.unwrap();
|
|
1155
1157
|
}
|
|
1156
1158
|
|
|
1159
|
+
fn seed_canonical_team_identity(workspace: &std::path::Path, team_key: &str) {
|
|
1160
|
+
let mut state = crate::state::persist::load_runtime_state(workspace).unwrap();
|
|
1161
|
+
state["active_team_key"] = json!(team_key);
|
|
1162
|
+
crate::state::persist::save_runtime_state(workspace, &state).unwrap();
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1157
1165
|
// RED — quick_start_with_transport must drive the REAL spawn path: launch.dry_run==false, started
|
|
1158
1166
|
// non-empty (one per compiled agent), and the transport records >=1 spawn carrying that agent's
|
|
1159
1167
|
// provider build_command. Today the dry_run=true bug -> launch.started empty + ZERO spawns recorded ->
|
|
@@ -2263,6 +2271,7 @@ fn add_agent_with_transport_spawns_new_worker_not_stub() {
|
|
|
2263
2271
|
let role_file = team.join("worker2-role.md"); // OUTSIDE agents/ -> not a duplicate of an existing agent
|
|
2264
2272
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
2265
2273
|
seed_healthy_coordinator(&team);
|
|
2274
|
+
seed_canonical_team_identity(&team, "addteam");
|
|
2266
2275
|
let transport = OfflineTransport::new();
|
|
2267
2276
|
|
|
2268
2277
|
let _result = add_agent_with_transport(
|
|
@@ -2473,6 +2482,10 @@ fn restart_allow_fresh_copilot_keeps_session_id_null_until_capture_confirms() {
|
|
|
2473
2482
|
.windows(2)
|
|
2474
2483
|
.find_map(|pair| (pair[0] == "--session-id").then(|| pair[1].clone()))
|
|
2475
2484
|
.expect("copilot fresh spawn argv must include --session-id");
|
|
2485
|
+
let instructions =
|
|
2486
|
+
std::fs::read_to_string(ws.join(".team/runtime/copilot-instructions/alpha/AGENTS.md"))
|
|
2487
|
+
.expect("restart/add/clone spawn must materialize Copilot instructions");
|
|
2488
|
+
assert!(instructions.contains("Alpha Worker"));
|
|
2476
2489
|
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
2477
2490
|
let agent = state.pointer("/agents/alpha").expect("alpha state");
|
|
2478
2491
|
// 0.4.6 contract: session_id stays null until scanner confirms sqlite.
|
|
@@ -3076,6 +3089,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3076
3089
|
"captured_at",
|
|
3077
3090
|
"captured_via",
|
|
3078
3091
|
"attribution_confidence",
|
|
3092
|
+
"capture_state",
|
|
3079
3093
|
"spawn_cwd",
|
|
3080
3094
|
"spawned_at",
|
|
3081
3095
|
"pane_id",
|
|
@@ -3331,6 +3345,7 @@ fn add_agent_reachability_gate_returns_error_when_spawn_pane_not_addressable() {
|
|
|
3331
3345
|
let role_file = team.join("worker2-role.md");
|
|
3332
3346
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
3333
3347
|
seed_healthy_coordinator(&team);
|
|
3348
|
+
seed_canonical_team_identity(&team, "reachgate");
|
|
3334
3349
|
|
|
3335
3350
|
// Models "spawn to wrong socket" / "spawn succeeded but pane orphan": the
|
|
3336
3351
|
// OfflineTransport records the spawn but reports the new pane is NOT
|
|
@@ -3390,6 +3405,7 @@ fn add_agent_reachability_gate_passes_when_spawn_pane_addressable() {
|
|
|
3390
3405
|
let role_file = team.join("worker2-role.md");
|
|
3391
3406
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
3392
3407
|
seed_healthy_coordinator(&team);
|
|
3408
|
+
seed_canonical_team_identity(&team, "gatepass");
|
|
3393
3409
|
|
|
3394
3410
|
// Default OfflineTransport: spawned_panes_addressable=true, liveness=Unknown
|
|
3395
3411
|
// (treated as not-Dead by the gate's fallthrough).
|
|
@@ -4149,6 +4165,7 @@ fn e42_re_add_and_remove_agree_on_existence_after_add() {
|
|
|
4149
4165
|
let role_file = team.join("newagent.md");
|
|
4150
4166
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
4151
4167
|
seed_healthy_coordinator(&team);
|
|
4168
|
+
seed_canonical_team_identity(&team, "e42sym");
|
|
4152
4169
|
let transport = OfflineTransport::new();
|
|
4153
4170
|
// (1) first add
|
|
4154
4171
|
let r1 = crate::lifecycle::add_agent_with_transport(
|
|
@@ -4205,6 +4222,7 @@ fn e42_add_writes_to_canonical_runtime_spec_and_state_consistently() {
|
|
|
4205
4222
|
let role_file = team.join("newagent.md");
|
|
4206
4223
|
std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
|
|
4207
4224
|
seed_healthy_coordinator(&team);
|
|
4225
|
+
seed_canonical_team_identity(&team, "e42proj");
|
|
4208
4226
|
let transport = OfflineTransport::new();
|
|
4209
4227
|
let _ = crate::lifecycle::add_agent_with_transport(
|
|
4210
4228
|
&team,
|
|
@@ -318,6 +318,7 @@ fn reset_agent_discard_session_rebuilds_window_via_start_respawn() {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
#[test]
|
|
321
|
+
#[serial_test::serial(env)]
|
|
321
322
|
fn reset_agent_discard_session_syncs_projection_epoch_inputs_for_restart_agent_command() {
|
|
322
323
|
let ws = restart_ws_two_resumable_workers();
|
|
323
324
|
let mut state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use super::agent_ops::lanea_team_ws;
|
|
2
|
-
use super::lane_ops::{fork_ws, LaneTransport};
|
|
2
|
+
use super::lane_ops::{fork_ws, LaneHomeGuard, LaneTransport};
|
|
3
3
|
use super::launch_spawn::{
|
|
4
4
|
quick_start_team_dir, seed_healthy_coordinator, DELEG_ROLE_ALPHA, QS_VALID_ROLE,
|
|
5
5
|
};
|
|
@@ -129,7 +129,9 @@ fn concurrent_reset_discard_session_serializes() {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
#[test]
|
|
132
|
+
#[serial_test::serial(env)]
|
|
132
133
|
fn add_fork_remove_share_lifecycle_lock_behavior() {
|
|
134
|
+
let _home = LaneHomeGuard::enter("phase-b-fork");
|
|
133
135
|
let (add_team, add_workspace, add_role, add_transport) = add_fixture();
|
|
134
136
|
let add_agent = aid("w2");
|
|
135
137
|
let add_state_before =
|