@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
|
@@ -1,55 +1,16 @@
|
|
|
1
|
-
use
|
|
2
|
-
use
|
|
3
|
-
use std::process::Command;
|
|
4
|
-
|
|
1
|
+
use super::*;
|
|
2
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
5
3
|
use crate::lifecycle::profile_launch::parse_provider;
|
|
6
4
|
use crate::lifecycle::*;
|
|
7
|
-
use crate::model::enums::{AuthMode, DisplayBackend,
|
|
5
|
+
use crate::model::enums::{AuthMode, DisplayBackend, Provider, ProviderEffort};
|
|
8
6
|
use crate::model::ids::AgentId;
|
|
9
7
|
use crate::model::permissions::{self, AgentPermissionInput};
|
|
10
8
|
use crate::model::yaml::{self, Value};
|
|
11
9
|
use crate::state::persist::load_runtime_state;
|
|
12
10
|
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
13
|
-
|
|
14
|
-
use
|
|
15
|
-
|
|
16
|
-
use super::*;
|
|
17
|
-
|
|
18
|
-
/// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
|
|
19
|
-
/// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
|
|
20
|
-
/// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
|
|
21
|
-
pub fn fork_agent(
|
|
22
|
-
workspace: &Path,
|
|
23
|
-
source_agent_id: &AgentId,
|
|
24
|
-
as_agent_id: &AgentId,
|
|
25
|
-
label: Option<&str>,
|
|
26
|
-
open_display: bool,
|
|
27
|
-
team: Option<&str>,
|
|
28
|
-
) -> Result<ForkAgentReport, LifecycleError> {
|
|
29
|
-
let selected = crate::state::selector::resolve_active_team(
|
|
30
|
-
workspace,
|
|
31
|
-
team,
|
|
32
|
-
crate::state::selector::SelectorMode::RequireSpec,
|
|
33
|
-
)
|
|
34
|
-
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
35
|
-
// **0.3.24 add-agent socket drift fix** (same root cause): fork-agent must
|
|
36
|
-
// also route to the live team's persisted tmux endpoint, not the workspace-
|
|
37
|
-
// hash for_workspace socket. Same orphan-on-wrong-socket pathology.
|
|
38
|
-
let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
39
|
-
&selected.run_workspace,
|
|
40
|
-
Some(selected.team_key.as_str()),
|
|
41
|
-
)
|
|
42
|
-
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
|
|
43
|
-
fork_agent_with_transport(
|
|
44
|
-
workspace,
|
|
45
|
-
source_agent_id,
|
|
46
|
-
as_agent_id,
|
|
47
|
-
label,
|
|
48
|
-
open_display,
|
|
49
|
-
team,
|
|
50
|
-
&transport,
|
|
51
|
-
)
|
|
52
|
-
}
|
|
11
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
12
|
+
use std::path::{Path, PathBuf};
|
|
13
|
+
use std::process::Command;
|
|
53
14
|
|
|
54
15
|
pub fn fork_agent_with_transport(
|
|
55
16
|
workspace: &Path,
|
|
@@ -104,12 +65,8 @@ pub fn fork_agent_with_transport(
|
|
|
104
65
|
let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
|
|
105
66
|
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
106
67
|
})?;
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// captured_at + captured_via) before treating the scalar session_id
|
|
110
|
-
// as resumable truth. A row carrying only `session_id` is a partial
|
|
111
|
-
// tuple (pre-0.4.6 bug source), and the native fork would attach to
|
|
112
|
-
// a session that has no confirmed backing.
|
|
68
|
+
// Fork requires the complete source tuple before treating session_id as
|
|
69
|
+
// resumable truth; a scalar-only row has no confirmed backing.
|
|
113
70
|
let source_agent_state = state
|
|
114
71
|
.get("agents")
|
|
115
72
|
.and_then(|v| v.get(source_agent_id.as_str()))
|
|
@@ -142,7 +99,24 @@ pub fn fork_agent_with_transport(
|
|
|
142
99
|
(session_id+rollout_path+captured_at+captured_via required)"
|
|
143
100
|
)));
|
|
144
101
|
}
|
|
145
|
-
let
|
|
102
|
+
let Some(source_backing_raw) = rollout_path_str else {
|
|
103
|
+
return Err(LifecycleError::Provider(format!(
|
|
104
|
+
"cannot fork {source_agent_id}: source session backing is missing"
|
|
105
|
+
)));
|
|
106
|
+
};
|
|
107
|
+
let source_backing = Path::new(source_backing_raw);
|
|
108
|
+
if !source_backing.is_file() {
|
|
109
|
+
return Err(LifecycleError::Provider(format!(
|
|
110
|
+
"cannot fork {source_agent_id}: source session backing is not readable: {}",
|
|
111
|
+
source_backing.display()
|
|
112
|
+
)));
|
|
113
|
+
}
|
|
114
|
+
let Some(source_session_id) = session_id_str else {
|
|
115
|
+
return Err(LifecycleError::Provider(format!(
|
|
116
|
+
"cannot fork {source_agent_id}: source session id is missing"
|
|
117
|
+
)));
|
|
118
|
+
};
|
|
119
|
+
let session_id = crate::provider::SessionId::new(source_session_id.to_string());
|
|
146
120
|
let session_name = state
|
|
147
121
|
.get("session_name")
|
|
148
122
|
.and_then(|v| v.as_str())
|
|
@@ -160,7 +134,24 @@ pub fn fork_agent_with_transport(
|
|
|
160
134
|
as_agent_id.as_str()
|
|
161
135
|
)));
|
|
162
136
|
}
|
|
163
|
-
let
|
|
137
|
+
let mut materialized_role = materialize_latest_role(
|
|
138
|
+
&workspace,
|
|
139
|
+
&fork_team_dir,
|
|
140
|
+
&state,
|
|
141
|
+
source_agent_id,
|
|
142
|
+
as_agent_id,
|
|
143
|
+
label,
|
|
144
|
+
)?;
|
|
145
|
+
clamp_materialized_role_to_leader(materialized_role.path(), &spec)?;
|
|
146
|
+
let team_meta = crate::compiler::read_front_matter(&fork_team_dir.join("TEAM.md"))
|
|
147
|
+
.map(|(meta, _)| meta)
|
|
148
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
149
|
+
let workspace_s = workspace.to_string_lossy().to_string();
|
|
150
|
+
let compiled =
|
|
151
|
+
crate::compiler::compile_role_agent(materialized_role.path(), &team_meta, &workspace_s)
|
|
152
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
153
|
+
let new_spec =
|
|
154
|
+
append_forked_agent(&spec, &compiled.agent, source_agent_id, as_agent_id, label)?;
|
|
164
155
|
// validate 用角色定义目录的 team_workspace(校验 working_directory),非 spec 落点。
|
|
165
156
|
let validate_ws =
|
|
166
157
|
crate::model::paths::team_workspace(&fork_team_dir).unwrap_or_else(|_| workspace.clone());
|
|
@@ -223,14 +214,43 @@ pub fn fork_agent_with_transport(
|
|
|
223
214
|
})?;
|
|
224
215
|
// E5 §3:profiles 随角色定义目录(team_dir),不随已迁出的 spec。
|
|
225
216
|
let profile_dir = fork_team_dir.join("profiles");
|
|
226
|
-
let profile_launch =
|
|
227
|
-
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
217
|
+
let mut profile_launch =
|
|
218
|
+
match crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
228
219
|
&workspace,
|
|
229
220
|
as_agent_id.as_str(),
|
|
230
221
|
new_agent,
|
|
231
222
|
Some(&profile_dir),
|
|
232
223
|
Some(&mcp_config),
|
|
233
|
-
)
|
|
224
|
+
) {
|
|
225
|
+
Ok(profile_launch) => profile_launch,
|
|
226
|
+
Err(error) => {
|
|
227
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
228
|
+
let _ = std::fs::remove_file(&mcp_config_path);
|
|
229
|
+
return Err(error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
let mut copilot_fork = None;
|
|
233
|
+
if provider == Provider::Copilot {
|
|
234
|
+
let materialized = crate::provider::adapters::copilot_fork::materialize_copilot_fork(
|
|
235
|
+
&workspace,
|
|
236
|
+
as_agent_id.as_str(),
|
|
237
|
+
&session_id,
|
|
238
|
+
)
|
|
239
|
+
.map_err(|error| {
|
|
240
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
241
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
242
|
+
LifecycleError::Provider(error.to_string())
|
|
243
|
+
})?;
|
|
244
|
+
profile_launch.env_overlay.insert(
|
|
245
|
+
"COPILOT_HOME".to_string(),
|
|
246
|
+
materialized.home().to_string_lossy().to_string(),
|
|
247
|
+
);
|
|
248
|
+
profile_launch.env_overlay.insert(
|
|
249
|
+
"TEAM_AGENT_INTERNAL_COPILOT_FORK_SESSION_ID".to_string(),
|
|
250
|
+
materialized.session_id().as_str().to_string(),
|
|
251
|
+
);
|
|
252
|
+
copilot_fork = Some(materialized);
|
|
253
|
+
}
|
|
234
254
|
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
235
255
|
// 0.4.x provider effort MVP: fork inherits effort from the new agent JSON
|
|
236
256
|
// (compiler.rs propagated the role/team effort into the agent at fork-spawn).
|
|
@@ -257,8 +277,12 @@ pub fn fork_agent_with_transport(
|
|
|
257
277
|
)
|
|
258
278
|
.map_err(|e| {
|
|
259
279
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
280
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
260
281
|
LifecycleError::Provider(e.to_string())
|
|
261
282
|
})?;
|
|
283
|
+
profile_launch
|
|
284
|
+
.env_overlay
|
|
285
|
+
.remove("TEAM_AGENT_INTERNAL_COPILOT_FORK_SESSION_ID");
|
|
262
286
|
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
263
287
|
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
264
288
|
}
|
|
@@ -268,15 +292,54 @@ pub fn fork_agent_with_transport(
|
|
|
268
292
|
as_agent_id.as_str(),
|
|
269
293
|
Some(&fork_team),
|
|
270
294
|
);
|
|
295
|
+
if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
296
|
+
plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
|
|
297
|
+
}
|
|
271
298
|
let window = WindowName::new(as_agent_id.as_str());
|
|
272
|
-
|
|
299
|
+
let backing_before = crate::provider::session::ContextBackingSnapshot::capture(provider, &plan);
|
|
300
|
+
let mut claude_fork = prepare_claude_fork_backing(provider, &plan, source_backing, &session_id)
|
|
301
|
+
.map_err(|error| {
|
|
302
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
303
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
304
|
+
error
|
|
305
|
+
})?;
|
|
273
306
|
let mut env =
|
|
274
307
|
inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
275
308
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
276
309
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
310
|
+
if provider == Provider::Copilot {
|
|
311
|
+
apply_copilot_instructions_overlay(
|
|
312
|
+
&workspace,
|
|
313
|
+
as_agent_id.as_str(),
|
|
314
|
+
&system_prompt,
|
|
315
|
+
&mut env,
|
|
316
|
+
)
|
|
317
|
+
.map_err(|error| {
|
|
318
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
319
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
320
|
+
error
|
|
321
|
+
})?;
|
|
322
|
+
}
|
|
323
|
+
let mut reserved_state = state.clone();
|
|
324
|
+
reserve_forked_agent_state(
|
|
325
|
+
&mut reserved_state,
|
|
326
|
+
source_agent_id,
|
|
327
|
+
as_agent_id,
|
|
328
|
+
new_agent,
|
|
329
|
+
materialized_role.path(),
|
|
330
|
+
)?;
|
|
331
|
+
if let Err(error) = crate::state::repository::StateRepository::new(&workspace).save(
|
|
332
|
+
crate::state::repository::StateWriteIntent::ForkAgent {
|
|
333
|
+
team_key: &fork_team,
|
|
334
|
+
agent_id: as_agent_id.as_str(),
|
|
335
|
+
},
|
|
336
|
+
&reserved_state,
|
|
337
|
+
) {
|
|
338
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
339
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
340
|
+
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
341
|
+
}
|
|
342
|
+
// Match launch/restart: absent session spawns first; otherwise add a window.
|
|
280
343
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
281
344
|
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
282
345
|
provider,
|
|
@@ -286,6 +349,10 @@ pub fn fork_agent_with_transport(
|
|
|
286
349
|
provider,
|
|
287
350
|
),
|
|
288
351
|
);
|
|
352
|
+
// Release the metadata lock before per-seat provider convergence; finalize reacquires it.
|
|
353
|
+
drop(_lock);
|
|
354
|
+
let spawn_epoch = 1_u64;
|
|
355
|
+
let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
|
|
289
356
|
let spawn_result = if session_live {
|
|
290
357
|
transport.spawn_into_with_env_unset(
|
|
291
358
|
&session_name,
|
|
@@ -305,104 +372,79 @@ pub fn fork_agent_with_transport(
|
|
|
305
372
|
&env_unset,
|
|
306
373
|
)
|
|
307
374
|
};
|
|
308
|
-
let spawn = spawn_result
|
|
309
|
-
|
|
310
|
-
|
|
375
|
+
let spawn = match spawn_result {
|
|
376
|
+
Ok(spawn) => spawn,
|
|
377
|
+
Err(error) => {
|
|
378
|
+
rollback_fork_after_spawn(
|
|
379
|
+
&workspace,
|
|
380
|
+
transport,
|
|
381
|
+
&session_name,
|
|
382
|
+
&window,
|
|
383
|
+
&mcp_config_path,
|
|
384
|
+
as_agent_id,
|
|
385
|
+
&profile_launch,
|
|
386
|
+
&fork_team,
|
|
387
|
+
);
|
|
388
|
+
return Err(LifecycleError::Transport(error.to_string()));
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
ensure_fork_spawn_live(ForkPostSpawnInput {
|
|
392
|
+
workspace: &workspace,
|
|
393
|
+
transport,
|
|
394
|
+
session_name: &session_name,
|
|
395
|
+
window: &window,
|
|
396
|
+
mcp_config_path: &mcp_config_path,
|
|
397
|
+
agent_id: as_agent_id,
|
|
398
|
+
profile_launch: &profile_launch,
|
|
399
|
+
team_key: &fork_team,
|
|
400
|
+
spawn: &spawn,
|
|
311
401
|
})?;
|
|
312
|
-
let
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
as_agent_id,
|
|
318
|
-
new_agent,
|
|
319
|
-
&safety,
|
|
402
|
+
let convergence_deadline =
|
|
403
|
+
crate::provider::session::context_fork_convergence_deadline(provider);
|
|
404
|
+
let context_proof = match crate::provider::session::verify_context_fork(
|
|
405
|
+
provider,
|
|
406
|
+
&session_id,
|
|
320
407
|
&plan,
|
|
321
|
-
&
|
|
322
|
-
|
|
408
|
+
&backing_before,
|
|
409
|
+
claude_fork.as_ref().map(|materialized| materialized.path()),
|
|
410
|
+
as_agent_id.as_str(),
|
|
323
411
|
&workspace,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if let Some(agent) = next_state
|
|
327
|
-
.get_mut("agents")
|
|
328
|
-
.and_then(serde_json::Value::as_object_mut)
|
|
329
|
-
.and_then(|agents| agents.get_mut(as_agent_id.as_str()))
|
|
330
|
-
.and_then(serde_json::Value::as_object_mut)
|
|
331
|
-
{
|
|
332
|
-
persist_effective_approval_policy(agent, &safety);
|
|
333
|
-
}
|
|
334
|
-
if let Err(e) = maybe_fail_fork_after_spawn("save_runtime_state") {
|
|
335
|
-
rollback_fork_after_spawn(
|
|
336
|
-
&workspace,
|
|
337
|
-
&spec_path,
|
|
338
|
-
&text,
|
|
339
|
-
&old_state,
|
|
340
|
-
transport,
|
|
341
|
-
&session_name,
|
|
342
|
-
&window,
|
|
343
|
-
&mcp_config_path,
|
|
344
|
-
as_agent_id,
|
|
345
|
-
&profile_launch,
|
|
346
|
-
&fork_team,
|
|
347
|
-
);
|
|
348
|
-
return Err(e);
|
|
349
|
-
}
|
|
350
|
-
if let Err(e) = crate::state::repository::StateRepository::new(&workspace).save(
|
|
351
|
-
crate::state::repository::StateWriteIntent::ForkAgent {
|
|
352
|
-
team_key: &fork_team,
|
|
353
|
-
agent_id: as_agent_id.as_str(),
|
|
354
|
-
},
|
|
355
|
-
&next_state,
|
|
412
|
+
&spawned_at,
|
|
413
|
+
convergence_deadline,
|
|
356
414
|
) {
|
|
415
|
+
Ok(proof) => proof,
|
|
416
|
+
Err(error) => {
|
|
417
|
+
rollback_fork_after_spawn(
|
|
418
|
+
&workspace,
|
|
419
|
+
transport,
|
|
420
|
+
&session_name,
|
|
421
|
+
&window,
|
|
422
|
+
&mcp_config_path,
|
|
423
|
+
as_agent_id,
|
|
424
|
+
&profile_launch,
|
|
425
|
+
&fork_team,
|
|
426
|
+
);
|
|
427
|
+
return Err(LifecycleError::Provider(error.to_string()));
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
if let Err(error) = finalize_fork_state(ForkFinalizeInput {
|
|
431
|
+
workspace: &workspace,
|
|
432
|
+
team_key: &fork_team,
|
|
433
|
+
source_agent_id,
|
|
434
|
+
agent_id: as_agent_id,
|
|
435
|
+
spec_agent: new_agent,
|
|
436
|
+
safety: &safety,
|
|
437
|
+
plan: &plan,
|
|
438
|
+
profile_launch: &profile_launch,
|
|
439
|
+
spawn: &spawn,
|
|
440
|
+
profile_dir: &profile_dir,
|
|
441
|
+
dynamic_role_file: materialized_role.path(),
|
|
442
|
+
context_proof: &context_proof,
|
|
443
|
+
spawned_at: &spawned_at,
|
|
444
|
+
spawn_epoch,
|
|
445
|
+
}) {
|
|
357
446
|
rollback_fork_after_spawn(
|
|
358
447
|
&workspace,
|
|
359
|
-
&spec_path,
|
|
360
|
-
&text,
|
|
361
|
-
&old_state,
|
|
362
|
-
transport,
|
|
363
|
-
&session_name,
|
|
364
|
-
&window,
|
|
365
|
-
&mcp_config_path,
|
|
366
|
-
as_agent_id,
|
|
367
|
-
&profile_launch,
|
|
368
|
-
&fork_team,
|
|
369
|
-
);
|
|
370
|
-
return Err(LifecycleError::StatePersist(e.to_string()));
|
|
371
|
-
}
|
|
372
|
-
let registration =
|
|
373
|
-
crate::state::projection::select_runtime_state(&workspace, Some(fork_team.as_str()))
|
|
374
|
-
.map_err(|e| e.to_string())
|
|
375
|
-
.and_then(|saved| {
|
|
376
|
-
let agent = saved
|
|
377
|
-
.get("agents")
|
|
378
|
-
.and_then(|agents| agents.get(as_agent_id.as_str()))
|
|
379
|
-
.ok_or_else(|| "canonical team row is missing".to_string())?;
|
|
380
|
-
if agent.get("pane_id").and_then(serde_json::Value::as_str)
|
|
381
|
-
!= Some(spawn.pane_id.as_str())
|
|
382
|
-
{
|
|
383
|
-
return Err("canonical team pane_id does not match spawned pane".to_string());
|
|
384
|
-
}
|
|
385
|
-
if agent.get("window").and_then(serde_json::Value::as_str) != Some(window.as_str())
|
|
386
|
-
{
|
|
387
|
-
return Err("canonical team window does not match spawned window".to_string());
|
|
388
|
-
}
|
|
389
|
-
if let Some(pid) = spawn.child_pid {
|
|
390
|
-
if agent.get("pane_pid").and_then(serde_json::Value::as_u64)
|
|
391
|
-
!= Some(u64::from(pid))
|
|
392
|
-
{
|
|
393
|
-
return Err(
|
|
394
|
-
"canonical team pane_pid does not match spawned process".to_string()
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
Ok(())
|
|
399
|
-
});
|
|
400
|
-
if let Err(reason) = registration {
|
|
401
|
-
rollback_fork_after_spawn(
|
|
402
|
-
&workspace,
|
|
403
|
-
&spec_path,
|
|
404
|
-
&text,
|
|
405
|
-
&old_state,
|
|
406
448
|
transport,
|
|
407
449
|
&session_name,
|
|
408
450
|
&window,
|
|
@@ -411,16 +453,13 @@ pub fn fork_agent_with_transport(
|
|
|
411
453
|
&profile_launch,
|
|
412
454
|
&fork_team,
|
|
413
455
|
);
|
|
414
|
-
return Err(
|
|
415
|
-
"fork spawned but team registration readback failed: {reason}"
|
|
416
|
-
)));
|
|
456
|
+
return Err(error);
|
|
417
457
|
}
|
|
418
|
-
if let Err(
|
|
458
|
+
if let Err(error) =
|
|
459
|
+
verify_fork_registration(&workspace, &fork_team, as_agent_id, &spawn, &window)
|
|
460
|
+
{
|
|
419
461
|
rollback_fork_after_spawn(
|
|
420
462
|
&workspace,
|
|
421
|
-
&spec_path,
|
|
422
|
-
&text,
|
|
423
|
-
&old_state,
|
|
424
463
|
transport,
|
|
425
464
|
&session_name,
|
|
426
465
|
&window,
|
|
@@ -429,28 +468,25 @@ pub fn fork_agent_with_transport(
|
|
|
429
468
|
&profile_launch,
|
|
430
469
|
&fork_team,
|
|
431
470
|
);
|
|
432
|
-
return Err(
|
|
471
|
+
return Err(error);
|
|
433
472
|
}
|
|
434
|
-
let coordinator_started =
|
|
435
|
-
&
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
&old_state,
|
|
444
|
-
transport,
|
|
445
|
-
&session_name,
|
|
446
|
-
&window,
|
|
447
|
-
&mcp_config_path,
|
|
448
|
-
as_agent_id,
|
|
449
|
-
&profile_launch,
|
|
450
|
-
&fork_team,
|
|
451
|
-
);
|
|
452
|
-
LifecycleError::StatePersist(e.to_string())
|
|
473
|
+
let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
|
|
474
|
+
workspace: &workspace,
|
|
475
|
+
team_key: &fork_team,
|
|
476
|
+
agent_id: as_agent_id,
|
|
477
|
+
transport,
|
|
478
|
+
session_name: &session_name,
|
|
479
|
+
window: &window,
|
|
480
|
+
mcp_config_path: &mcp_config_path,
|
|
481
|
+
profile_launch: &profile_launch,
|
|
453
482
|
})?;
|
|
483
|
+
materialized_role.keep();
|
|
484
|
+
if let Some(materialized) = claude_fork.as_mut() {
|
|
485
|
+
materialized.keep();
|
|
486
|
+
}
|
|
487
|
+
if let Some(materialized) = copilot_fork.as_mut() {
|
|
488
|
+
materialized.keep();
|
|
489
|
+
}
|
|
454
490
|
Ok(ForkAgentReport {
|
|
455
491
|
source_agent_id: source_agent_id.clone(),
|
|
456
492
|
new_agent_id: as_agent_id.clone(),
|
|
@@ -459,34 +495,6 @@ pub fn fork_agent_with_transport(
|
|
|
459
495
|
state_file: crate::state::persist::runtime_state_path(&workspace),
|
|
460
496
|
coordinator_started,
|
|
461
497
|
},
|
|
462
|
-
session_id:
|
|
498
|
+
session_id: Some(context_proof.new_session_id),
|
|
463
499
|
})
|
|
464
500
|
}
|
|
465
|
-
|
|
466
|
-
pub(super) fn rollback_fork_after_spawn(
|
|
467
|
-
workspace: &Path,
|
|
468
|
-
spec_path: &Path,
|
|
469
|
-
spec_text: &str,
|
|
470
|
-
old_state: &serde_json::Value,
|
|
471
|
-
transport: &dyn Transport,
|
|
472
|
-
session_name: &SessionName,
|
|
473
|
-
window: &WindowName,
|
|
474
|
-
mcp_config_path: &Path,
|
|
475
|
-
agent_id: &AgentId,
|
|
476
|
-
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
477
|
-
team_key: &str,
|
|
478
|
-
) {
|
|
479
|
-
let _ = transport.kill_window(&Target::SessionWindow {
|
|
480
|
-
session: session_name.clone(),
|
|
481
|
-
window: window.clone(),
|
|
482
|
-
});
|
|
483
|
-
let _ = std::fs::write(spec_path, spec_text.as_bytes());
|
|
484
|
-
let _ = crate::state::repository::StateRepository::new(workspace).save(
|
|
485
|
-
crate::state::repository::StateWriteIntent::AgentRollback {
|
|
486
|
-
team_key: Some(team_key),
|
|
487
|
-
agent_id: agent_id.as_str(),
|
|
488
|
-
},
|
|
489
|
-
old_state,
|
|
490
|
-
);
|
|
491
|
-
cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
|
|
492
|
-
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
/// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
|
|
4
|
+
/// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
|
|
5
|
+
/// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
|
|
6
|
+
pub fn fork_agent(
|
|
7
|
+
workspace: &Path,
|
|
8
|
+
source_agent_id: &AgentId,
|
|
9
|
+
as_agent_id: &AgentId,
|
|
10
|
+
label: Option<&str>,
|
|
11
|
+
open_display: bool,
|
|
12
|
+
team: Option<&str>,
|
|
13
|
+
) -> Result<ForkAgentReport, LifecycleError> {
|
|
14
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
15
|
+
workspace,
|
|
16
|
+
team,
|
|
17
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
18
|
+
)
|
|
19
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
20
|
+
// Fork-agent routes to the selected live team's persisted endpoint, not
|
|
21
|
+
// the workspace-hash fallback socket.
|
|
22
|
+
let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
23
|
+
&selected.run_workspace,
|
|
24
|
+
Some(selected.team_key.as_str()),
|
|
25
|
+
)
|
|
26
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
|
|
27
|
+
fork_agent_with_transport(
|
|
28
|
+
workspace,
|
|
29
|
+
source_agent_id,
|
|
30
|
+
as_agent_id,
|
|
31
|
+
label,
|
|
32
|
+
open_display,
|
|
33
|
+
team,
|
|
34
|
+
&transport,
|
|
35
|
+
)
|
|
36
|
+
}
|