@team-agent/installer 0.5.52 → 0.5.54
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 +111 -3
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +12 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- 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 +324 -57
- 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 +5 -0
- 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/role_source.rs +170 -0
- 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 +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -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/tests/phase_golden.rs +1 -0
- 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/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +89 -4
- package/crates/team-agent/src/mcp_server/types.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +65 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +100 -21
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +85 -24
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +1 -0
- 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 +395 -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/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +53 -92
- 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/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- package/skills/team-agent/SKILL.md +11 -3
|
@@ -449,6 +449,15 @@ fn restart_with_selected_team_and_transport(
|
|
|
449
449
|
moved leader to dedicated session)",
|
|
450
450
|
session_name.as_str()
|
|
451
451
|
);
|
|
452
|
+
let targets = vec![session_name.as_str().to_string()];
|
|
453
|
+
crate::kill_audit::pre_kill_audit(
|
|
454
|
+
&crate::event_log::EventLog::new(&selected.run_workspace),
|
|
455
|
+
transport,
|
|
456
|
+
"restart.stale_worker_session",
|
|
457
|
+
crate::kill_audit::KILL_SESSION,
|
|
458
|
+
&targets,
|
|
459
|
+
)
|
|
460
|
+
.map_err(|error| LifecycleError::Transport(format!("pre-kill audit failed: {error}")))?;
|
|
452
461
|
transport
|
|
453
462
|
.kill_session(&session_name)
|
|
454
463
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
@@ -1984,7 +1993,7 @@ fn mark_agent_respawned(
|
|
|
1984
1993
|
}
|
|
1985
1994
|
agent.insert(
|
|
1986
1995
|
"spawned_at".to_string(),
|
|
1987
|
-
serde_json::json!(
|
|
1996
|
+
serde_json::json!(spawn.spawned_at.as_str()),
|
|
1988
1997
|
);
|
|
1989
1998
|
agent.insert(
|
|
1990
1999
|
"spawn_cwd".to_string(),
|
|
@@ -2462,6 +2471,7 @@ impl Clone for ParallelSpawnResult {
|
|
|
2462
2471
|
Self {
|
|
2463
2472
|
spawn: SpawnedAgentWindow {
|
|
2464
2473
|
spawn: self.spawn.spawn.clone(),
|
|
2474
|
+
spawned_at: self.spawn.spawned_at.clone(),
|
|
2465
2475
|
plan: self.spawn.plan.clone(),
|
|
2466
2476
|
profile_launch: self.spawn.profile_launch.clone(),
|
|
2467
2477
|
layout_placement: self.spawn.layout_placement.clone(),
|
|
@@ -3689,6 +3699,7 @@ tasks:
|
|
|
3689
3699
|
}
|
|
3690
3700
|
}
|
|
3691
3701
|
});
|
|
3702
|
+
let before = chrono::Utc::now();
|
|
3692
3703
|
let spawn = SpawnedAgentWindow {
|
|
3693
3704
|
spawn: SpawnResult {
|
|
3694
3705
|
pane_id: PaneId::new("%new"),
|
|
@@ -3696,14 +3707,13 @@ tasks:
|
|
|
3696
3707
|
window: WindowName::new("w1"),
|
|
3697
3708
|
child_pid: Some(2020),
|
|
3698
3709
|
},
|
|
3710
|
+
spawned_at: chrono::Utc::now().to_rfc3339(),
|
|
3699
3711
|
plan: crate::provider::CommandPlan::argv_only(vec!["codex".to_string()]),
|
|
3700
3712
|
profile_launch: crate::provider::ProviderProfileLaunch::default(),
|
|
3701
3713
|
layout_placement: None,
|
|
3702
3714
|
spawn_cwd: std::path::PathBuf::from("/tmp/team-epoch"),
|
|
3703
3715
|
owner_team_id: None,
|
|
3704
3716
|
};
|
|
3705
|
-
let before = chrono::Utc::now();
|
|
3706
|
-
|
|
3707
3717
|
mark_agent_respawned(
|
|
3708
3718
|
&mut state,
|
|
3709
3719
|
&AgentId::new("w1"),
|
|
@@ -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,
|
|
@@ -1594,14 +1663,11 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
|
1594
1663
|
);
|
|
1595
1664
|
}
|
|
1596
1665
|
|
|
1597
|
-
//
|
|
1598
|
-
// Golden operations.py:399,408 returns state['agents'][as_agent_id].get('session_id') — the captured
|
|
1599
|
-
// provider session id (or None if capture missed, raise_on_missed=False). Rust sets
|
|
1600
|
-
// session_id: Some(SessionId::new(spawn.pane_id)) (launch.rs:502) — the tmux pane id ('%newfork'),
|
|
1601
|
-
// a different value kind. RED: the report session_id must NOT be the pane id (None, since the Rust fork
|
|
1602
|
-
// path performs no session capture).
|
|
1666
|
+
// Successful fork reports the verified provider session id, never the pane id.
|
|
1603
1667
|
#[test]
|
|
1668
|
+
#[serial_test::serial(env)]
|
|
1604
1669
|
fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
1670
|
+
let _home = LaneHomeGuard::enter("fork-report");
|
|
1605
1671
|
let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
|
|
1606
1672
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
1607
1673
|
let report =
|
|
@@ -1613,9 +1679,41 @@ fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
|
1613
1679
|
"golden operations.py:399,408: report.session_id is the captured provider session id / None, NEVER the \
|
|
1614
1680
|
tmux pane id; Rust returns Some(pane_id='%newfork')"
|
|
1615
1681
|
);
|
|
1616
|
-
|
|
1617
|
-
report.session_id,
|
|
1618
|
-
"
|
|
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}"
|
|
1619
1717
|
);
|
|
1620
1718
|
}
|
|
1621
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();
|
|
@@ -2481,6 +2482,10 @@ fn restart_allow_fresh_copilot_keeps_session_id_null_until_capture_confirms() {
|
|
|
2481
2482
|
.windows(2)
|
|
2482
2483
|
.find_map(|pair| (pair[0] == "--session-id").then(|| pair[1].clone()))
|
|
2483
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"));
|
|
2484
2489
|
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
2485
2490
|
let agent = state.pointer("/agents/alpha").expect("alpha state");
|
|
2486
2491
|
// 0.4.6 contract: session_id stays null until scanner confirms sqlite.
|
|
@@ -3084,6 +3089,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3084
3089
|
"captured_at",
|
|
3085
3090
|
"captured_via",
|
|
3086
3091
|
"attribution_confidence",
|
|
3092
|
+
"capture_state",
|
|
3087
3093
|
"spawn_cwd",
|
|
3088
3094
|
"spawned_at",
|
|
3089
3095
|
"pane_id",
|
|
@@ -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 =
|
|
@@ -267,6 +267,7 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
267
267
|
confirm_human: false,
|
|
268
268
|
json: true,
|
|
269
269
|
message_id: Some("phase-golden-message".to_string()),
|
|
270
|
+
presentation: crate::messaging::presentation::PresentationRequest::default(),
|
|
270
271
|
pane: None,
|
|
271
272
|
to_name: None,
|
|
272
273
|
to_leader: None,
|
|
@@ -666,6 +666,23 @@ pub struct ForkAgentReport {
|
|
|
666
666
|
pub session_id: Option<SessionId>,
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
670
|
+
pub struct CloneAgentReport {
|
|
671
|
+
pub source_agent_id: AgentId,
|
|
672
|
+
pub new_agent_id: AgentId,
|
|
673
|
+
pub env: AgentActionEnvelope,
|
|
674
|
+
pub session_id: Option<SessionId>,
|
|
675
|
+
pub backing_path: Option<PathBuf>,
|
|
676
|
+
pub backing_state: CloneBackingState,
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
680
|
+
#[serde(rename_all = "snake_case")]
|
|
681
|
+
pub enum CloneBackingState {
|
|
682
|
+
Verified,
|
|
683
|
+
PendingFirstTurn,
|
|
684
|
+
}
|
|
685
|
+
|
|
669
686
|
/// Read-only remove-agent flag requirements for the target's current state.
|
|
670
687
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
671
688
|
pub struct RemoveAgentFlagRequirements {
|
|
@@ -134,6 +134,41 @@ pub(crate) fn fork_agent(
|
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
pub(crate) fn clone_agent(
|
|
138
|
+
workspace: &Path,
|
|
139
|
+
owner_team: Option<&TeamKey>,
|
|
140
|
+
source_agent_id: &str,
|
|
141
|
+
as_agent_id: &str,
|
|
142
|
+
label: Option<&str>,
|
|
143
|
+
) -> ToolResult {
|
|
144
|
+
emit_newer_daemon_preserved_if_present(workspace)?;
|
|
145
|
+
let lifecycle_workspace = lifecycle_workspace(workspace, owner_team, false)?;
|
|
146
|
+
let report = crate::lifecycle::launch::clone_agent(
|
|
147
|
+
&lifecycle_workspace,
|
|
148
|
+
&AgentId::new(source_agent_id),
|
|
149
|
+
&AgentId::new(as_agent_id),
|
|
150
|
+
label,
|
|
151
|
+
false,
|
|
152
|
+
owner_team.map(TeamKey::as_str),
|
|
153
|
+
)
|
|
154
|
+
.map_err(tool_runtime_error)?;
|
|
155
|
+
Ok(ToolOk {
|
|
156
|
+
fields: object_fields(serde_json::json!({
|
|
157
|
+
"ok": true,
|
|
158
|
+
"status": "cloned",
|
|
159
|
+
"source_agent_id": report.source_agent_id.as_str(),
|
|
160
|
+
"agent_id": report.new_agent_id.as_str(),
|
|
161
|
+
"new_agent_id": report.new_agent_id.as_str(),
|
|
162
|
+
"state_file": report.env.state_file.to_string_lossy().to_string(),
|
|
163
|
+
"coordinator_started": report.env.coordinator_started,
|
|
164
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
165
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
166
|
+
"backing_path": report.backing_path.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
167
|
+
"backing_state": report.backing_state,
|
|
168
|
+
})),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
137
172
|
fn reset_refusal_reason(reason: ResetRefusal) -> Value {
|
|
138
173
|
match reason {
|
|
139
174
|
ResetRefusal::DiscardSessionRequired => {
|
|
@@ -440,9 +475,9 @@ fn prepare_selected_team_state(
|
|
|
440
475
|
state,
|
|
441
476
|
)
|
|
442
477
|
.map_err(|e| {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
478
|
+
tool_runtime_error(format!(
|
|
479
|
+
"save MCP lifecycle scoped state {}: {e}",
|
|
480
|
+
workspace.display()
|
|
481
|
+
))
|
|
482
|
+
})
|
|
448
483
|
}
|
|
@@ -104,6 +104,12 @@ pub fn normalize_report_envelope(env: &Value) -> NormalizedReportEnvelope {
|
|
|
104
104
|
let summary = text_field(env, "summary").unwrap_or_else(|| "completed".to_string());
|
|
105
105
|
let task_id = text_field(env, "task_id").unwrap_or_else(|| "manual".to_string());
|
|
106
106
|
let agent_id = text_field(env, "agent_id").unwrap_or_else(|| "unknown".to_string());
|
|
107
|
+
let (presentation_request, presentation_error) =
|
|
108
|
+
crate::messaging::presentation::normalize_report_presentation(env.get("presentation"));
|
|
109
|
+
let presentation = crate::messaging::presentation::decide_presentation(
|
|
110
|
+
&presentation_request,
|
|
111
|
+
crate::messaging::presentation::PresentationSource::ReportResult,
|
|
112
|
+
);
|
|
107
113
|
NormalizedReportEnvelope {
|
|
108
114
|
schema_version: "result_envelope_v1".to_string(),
|
|
109
115
|
task_id: TaskId::new(task_id),
|
|
@@ -115,6 +121,8 @@ pub fn normalize_report_envelope(env: &Value) -> NormalizedReportEnvelope {
|
|
|
115
121
|
risks: normalize_risks(env.get("risks")),
|
|
116
122
|
artifacts: normalize_artifacts(env.get("artifacts")),
|
|
117
123
|
next_actions: normalize_next_actions(env.get("next_actions")),
|
|
124
|
+
presentation,
|
|
125
|
+
presentation_error,
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|