@team-agent/installer 0.5.49 → 0.5.51
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 +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::profile_launch::parse_provider;
|
|
6
|
+
use crate::lifecycle::*;
|
|
7
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
8
|
+
use crate::model::ids::AgentId;
|
|
9
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
10
|
+
use crate::model::yaml::{self, Value};
|
|
11
|
+
use crate::state::persist::load_runtime_state;
|
|
12
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
13
|
+
|
|
14
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn fork_agent_with_transport(
|
|
55
|
+
workspace: &Path,
|
|
56
|
+
source_agent_id: &AgentId,
|
|
57
|
+
as_agent_id: &AgentId,
|
|
58
|
+
label: Option<&str>,
|
|
59
|
+
open_display: bool,
|
|
60
|
+
team: Option<&str>,
|
|
61
|
+
transport: &dyn Transport,
|
|
62
|
+
) -> Result<ForkAgentReport, LifecycleError> {
|
|
63
|
+
let _ = open_display;
|
|
64
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
65
|
+
workspace,
|
|
66
|
+
team,
|
|
67
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
68
|
+
)
|
|
69
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
70
|
+
let lock_workspace = selected.run_workspace.clone();
|
|
71
|
+
let lock_team_key = selected.team_key.clone();
|
|
72
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
73
|
+
workspace: &lock_workspace,
|
|
74
|
+
operation: "fork-agent",
|
|
75
|
+
team: Some(lock_team_key.as_str()),
|
|
76
|
+
agent_id: Some(as_agent_id),
|
|
77
|
+
})?;
|
|
78
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
79
|
+
&lock_workspace,
|
|
80
|
+
Some(lock_team_key.as_str()),
|
|
81
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
82
|
+
)
|
|
83
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
84
|
+
// E5 §3:team_dir(角色定义+profiles)恒用户目录。spec 读用 selector 解析的 spec_path
|
|
85
|
+
// (读序 B:runtime 优先、legacy 回落),写恒走 runtime_spec_path(canonical 落点)。
|
|
86
|
+
let fork_team_dir = selected.team_dir.clone();
|
|
87
|
+
let fork_team = selected.team_key.clone();
|
|
88
|
+
let read_spec_path = selected
|
|
89
|
+
.spec_path
|
|
90
|
+
.clone()
|
|
91
|
+
.ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
|
|
92
|
+
let workspace = selected.run_workspace;
|
|
93
|
+
let state = selected.state;
|
|
94
|
+
ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
|
|
95
|
+
let spec_path = crate::model::paths::runtime_spec_path(&workspace, &fork_team);
|
|
96
|
+
let text = std::fs::read_to_string(&read_spec_path)
|
|
97
|
+
.map_err(|e| LifecycleError::Compile(format!("{}: {e}", read_spec_path.display())))?;
|
|
98
|
+
let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
99
|
+
if find_spec_agent(&spec, as_agent_id).is_some() || leader_id_matches(&spec, as_agent_id) {
|
|
100
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
101
|
+
"agent id already exists: {as_agent_id}"
|
|
102
|
+
)));
|
|
103
|
+
}
|
|
104
|
+
let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
|
|
105
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
106
|
+
})?;
|
|
107
|
+
// 0.4.6 tuple-atomic contract (audit §Fork 修改清单, line 201): fork
|
|
108
|
+
// must require the COMPLETE source tuple (session_id + rollout_path +
|
|
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.
|
|
113
|
+
let source_agent_state = state
|
|
114
|
+
.get("agents")
|
|
115
|
+
.and_then(|v| v.get(source_agent_id.as_str()))
|
|
116
|
+
.ok_or_else(|| {
|
|
117
|
+
LifecycleError::Provider(format!(
|
|
118
|
+
"cannot fork {source_agent_id}: source agent row not in state"
|
|
119
|
+
))
|
|
120
|
+
})?;
|
|
121
|
+
let tuple_field_ok = |field: &str| -> bool {
|
|
122
|
+
source_agent_state
|
|
123
|
+
.get(field)
|
|
124
|
+
.and_then(|v| v.as_str())
|
|
125
|
+
.is_some_and(|s| !s.is_empty())
|
|
126
|
+
};
|
|
127
|
+
let session_id_str = source_agent_state
|
|
128
|
+
.get("session_id")
|
|
129
|
+
.and_then(|v| v.as_str())
|
|
130
|
+
.filter(|s| !s.is_empty());
|
|
131
|
+
let rollout_path_str = source_agent_state
|
|
132
|
+
.get("rollout_path")
|
|
133
|
+
.and_then(|v| v.as_str())
|
|
134
|
+
.filter(|s| !s.is_empty());
|
|
135
|
+
if session_id_str.is_none()
|
|
136
|
+
|| rollout_path_str.is_none()
|
|
137
|
+
|| !tuple_field_ok("captured_at")
|
|
138
|
+
|| !tuple_field_ok("captured_via")
|
|
139
|
+
{
|
|
140
|
+
return Err(LifecycleError::Provider(format!(
|
|
141
|
+
"cannot fork {source_agent_id}: source session backing is missing or incomplete \
|
|
142
|
+
(session_id+rollout_path+captured_at+captured_via required)"
|
|
143
|
+
)));
|
|
144
|
+
}
|
|
145
|
+
let session_id = crate::provider::SessionId::new(session_id_str.unwrap().to_string());
|
|
146
|
+
let session_name = state
|
|
147
|
+
.get("session_name")
|
|
148
|
+
.and_then(|v| v.as_str())
|
|
149
|
+
.filter(|s| !s.is_empty())
|
|
150
|
+
.map(SessionName::new)
|
|
151
|
+
.unwrap_or_else(|| spec_session_name(&spec));
|
|
152
|
+
if transport
|
|
153
|
+
.list_windows(&session_name)
|
|
154
|
+
.map(|windows| windows.iter().any(|w| w.as_str() == as_agent_id.as_str()))
|
|
155
|
+
.unwrap_or(false)
|
|
156
|
+
{
|
|
157
|
+
return Err(LifecycleError::Transport(format!(
|
|
158
|
+
"tmux window already exists for fork target: {}:{}",
|
|
159
|
+
session_name.as_str(),
|
|
160
|
+
as_agent_id.as_str()
|
|
161
|
+
)));
|
|
162
|
+
}
|
|
163
|
+
let new_spec = append_forked_agent(&spec, source_agent, source_agent_id, as_agent_id, label)?;
|
|
164
|
+
// validate 用角色定义目录的 team_workspace(校验 working_directory),非 spec 落点。
|
|
165
|
+
let validate_ws =
|
|
166
|
+
crate::model::paths::team_workspace(&fork_team_dir).unwrap_or_else(|_| workspace.clone());
|
|
167
|
+
crate::model::spec::validate_spec(&new_spec, &validate_ws)
|
|
168
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
169
|
+
write_spec_atomic(&spec_path, &new_spec)?;
|
|
170
|
+
let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
|
|
171
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
|
|
172
|
+
})?;
|
|
173
|
+
let provider = new_agent
|
|
174
|
+
.get("provider")
|
|
175
|
+
.and_then(Value::as_str)
|
|
176
|
+
.and_then(parse_provider)
|
|
177
|
+
.unwrap_or(Provider::Codex);
|
|
178
|
+
let auth_mode = new_agent
|
|
179
|
+
.get("auth_mode")
|
|
180
|
+
.and_then(Value::as_str)
|
|
181
|
+
.and_then(parse_auth_mode)
|
|
182
|
+
.unwrap_or(AuthMode::Subscription);
|
|
183
|
+
let adapter = crate::provider::get_adapter(provider);
|
|
184
|
+
let provider_str = new_agent
|
|
185
|
+
.get("provider")
|
|
186
|
+
.and_then(Value::as_str)
|
|
187
|
+
.unwrap_or("codex");
|
|
188
|
+
if auth_mode == AuthMode::CompatibleApi || !adapter.caps().fork {
|
|
189
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
190
|
+
return Err(LifecycleError::Provider(format!(
|
|
191
|
+
"{provider_str} does not support native session fork"
|
|
192
|
+
)));
|
|
193
|
+
}
|
|
194
|
+
let model = new_agent.get("model").and_then(Value::as_str);
|
|
195
|
+
let safety = effective_runtime_config(&new_spec)?;
|
|
196
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
197
|
+
new_agent,
|
|
198
|
+
Some(as_agent_id.as_str()),
|
|
199
|
+
provider,
|
|
200
|
+
);
|
|
201
|
+
let system_prompt =
|
|
202
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
203
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
204
|
+
&command_agent,
|
|
205
|
+
provider,
|
|
206
|
+
&safety,
|
|
207
|
+
)?;
|
|
208
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
209
|
+
let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
|
|
210
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
211
|
+
LifecycleError::Provider(e.to_string())
|
|
212
|
+
})?;
|
|
213
|
+
let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
|
|
214
|
+
let mcp_config_path = write_worker_mcp_config_for_provider(
|
|
215
|
+
&workspace,
|
|
216
|
+
as_agent_id.as_str(),
|
|
217
|
+
&mcp_config,
|
|
218
|
+
Some(provider),
|
|
219
|
+
)
|
|
220
|
+
.map_err(|e| {
|
|
221
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
222
|
+
e
|
|
223
|
+
})?;
|
|
224
|
+
// E5 §3:profiles 随角色定义目录(team_dir),不随已迁出的 spec。
|
|
225
|
+
let profile_dir = fork_team_dir.join("profiles");
|
|
226
|
+
let profile_launch =
|
|
227
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
228
|
+
&workspace,
|
|
229
|
+
as_agent_id.as_str(),
|
|
230
|
+
new_agent,
|
|
231
|
+
Some(&profile_dir),
|
|
232
|
+
Some(&mcp_config),
|
|
233
|
+
)?;
|
|
234
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
235
|
+
// 0.4.x provider effort MVP: fork inherits effort from the new agent JSON
|
|
236
|
+
// (compiler.rs propagated the role/team effort into the agent at fork-spawn).
|
|
237
|
+
let fork_effort = provider_effort_for_spawn(new_agent, provider);
|
|
238
|
+
if let Some(event_value) =
|
|
239
|
+
provider_effort_event_if_dropped(new_agent, provider, as_agent_id.as_str())
|
|
240
|
+
{
|
|
241
|
+
let _ = crate::event_log::EventLog::new(&workspace)
|
|
242
|
+
.write("provider.effort_unsupported", event_value);
|
|
243
|
+
}
|
|
244
|
+
let mut plan = adapter
|
|
245
|
+
.fork_plan(
|
|
246
|
+
Some(&session_id),
|
|
247
|
+
crate::provider::ProviderCommandContext {
|
|
248
|
+
auth_mode,
|
|
249
|
+
mcp_config: Some(&mcp_config),
|
|
250
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
251
|
+
model: command_model,
|
|
252
|
+
tools: &resolved_tool_refs,
|
|
253
|
+
profile_launch: Some(&profile_launch),
|
|
254
|
+
agent_id_hint: Some(as_agent_id.as_str()),
|
|
255
|
+
effort: fork_effort,
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
.map_err(|e| {
|
|
259
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
260
|
+
LifecycleError::Provider(e.to_string())
|
|
261
|
+
})?;
|
|
262
|
+
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
263
|
+
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
264
|
+
}
|
|
265
|
+
fill_spawn_placeholders_full(
|
|
266
|
+
&mut plan.argv,
|
|
267
|
+
&workspace,
|
|
268
|
+
as_agent_id.as_str(),
|
|
269
|
+
Some(&fork_team),
|
|
270
|
+
);
|
|
271
|
+
let window = WindowName::new(as_agent_id.as_str());
|
|
272
|
+
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
273
|
+
let mut env =
|
|
274
|
+
inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
275
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
276
|
+
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
277
|
+
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
278
|
+
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
279
|
+
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
280
|
+
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
281
|
+
let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
|
|
282
|
+
provider,
|
|
283
|
+
&mut env,
|
|
284
|
+
extend_worker_env_unset_for_effort(
|
|
285
|
+
profile_launch.env_unset.iter().cloned().collect(),
|
|
286
|
+
provider,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
let spawn_result = if session_live {
|
|
290
|
+
transport.spawn_into_with_env_unset(
|
|
291
|
+
&session_name,
|
|
292
|
+
&window,
|
|
293
|
+
&plan.argv,
|
|
294
|
+
&workspace,
|
|
295
|
+
&env,
|
|
296
|
+
&env_unset,
|
|
297
|
+
)
|
|
298
|
+
} else {
|
|
299
|
+
transport.spawn_first_with_env_unset(
|
|
300
|
+
&session_name,
|
|
301
|
+
&window,
|
|
302
|
+
&plan.argv,
|
|
303
|
+
&workspace,
|
|
304
|
+
&env,
|
|
305
|
+
&env_unset,
|
|
306
|
+
)
|
|
307
|
+
};
|
|
308
|
+
let spawn = spawn_result.map_err(|e| {
|
|
309
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
310
|
+
LifecycleError::Transport(e.to_string())
|
|
311
|
+
})?;
|
|
312
|
+
let old_state = state.clone();
|
|
313
|
+
let mut next_state = state;
|
|
314
|
+
upsert_forked_agent_state(
|
|
315
|
+
&mut next_state,
|
|
316
|
+
source_agent_id,
|
|
317
|
+
as_agent_id,
|
|
318
|
+
new_agent,
|
|
319
|
+
&safety,
|
|
320
|
+
&plan,
|
|
321
|
+
&profile_launch,
|
|
322
|
+
&spawn,
|
|
323
|
+
&workspace,
|
|
324
|
+
Some(&profile_dir),
|
|
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,
|
|
356
|
+
) {
|
|
357
|
+
rollback_fork_after_spawn(
|
|
358
|
+
&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
|
+
transport,
|
|
407
|
+
&session_name,
|
|
408
|
+
&window,
|
|
409
|
+
&mcp_config_path,
|
|
410
|
+
as_agent_id,
|
|
411
|
+
&profile_launch,
|
|
412
|
+
&fork_team,
|
|
413
|
+
);
|
|
414
|
+
return Err(LifecycleError::StatePersist(format!(
|
|
415
|
+
"fork spawned but team registration readback failed: {reason}"
|
|
416
|
+
)));
|
|
417
|
+
}
|
|
418
|
+
if let Err(e) = maybe_fail_fork_after_spawn("start_coordinator") {
|
|
419
|
+
rollback_fork_after_spawn(
|
|
420
|
+
&workspace,
|
|
421
|
+
&spec_path,
|
|
422
|
+
&text,
|
|
423
|
+
&old_state,
|
|
424
|
+
transport,
|
|
425
|
+
&session_name,
|
|
426
|
+
&window,
|
|
427
|
+
&mcp_config_path,
|
|
428
|
+
as_agent_id,
|
|
429
|
+
&profile_launch,
|
|
430
|
+
&fork_team,
|
|
431
|
+
);
|
|
432
|
+
return Err(e);
|
|
433
|
+
}
|
|
434
|
+
let coordinator_started = crate::coordinator::start_coordinator(
|
|
435
|
+
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
436
|
+
)
|
|
437
|
+
.map(|report| report.ok)
|
|
438
|
+
.map_err(|e| {
|
|
439
|
+
rollback_fork_after_spawn(
|
|
440
|
+
&workspace,
|
|
441
|
+
&spec_path,
|
|
442
|
+
&text,
|
|
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())
|
|
453
|
+
})?;
|
|
454
|
+
Ok(ForkAgentReport {
|
|
455
|
+
source_agent_id: source_agent_id.clone(),
|
|
456
|
+
new_agent_id: as_agent_id.clone(),
|
|
457
|
+
env: AgentActionEnvelope {
|
|
458
|
+
agent_id: as_agent_id.clone(),
|
|
459
|
+
state_file: crate::state::persist::runtime_state_path(&workspace),
|
|
460
|
+
coordinator_started,
|
|
461
|
+
},
|
|
462
|
+
session_id: None,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
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
|
+
}
|