@team-agent/installer 0.4.11 → 0.5.0
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 +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +62 -4
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
use super::*;
|
|
2
1
|
use super::common::*;
|
|
3
2
|
use super::selection::decide_start_mode;
|
|
4
3
|
use super::team_state::write_team_state;
|
|
4
|
+
use super::*;
|
|
5
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
5
6
|
|
|
6
7
|
/// `start_agent(workspace, agent_id, force, open_display, allow_fresh, team)`
|
|
7
8
|
/// (`lifecycle/start.py:72`)。`_runtime_lock("start-agent")` 下串行:resume-or-fresh
|
|
@@ -17,8 +18,13 @@ pub fn start_agent(
|
|
|
17
18
|
team: Option<&str>,
|
|
18
19
|
) -> Result<StartAgentOutcome, LifecycleError> {
|
|
19
20
|
let paths = lifecycle_paths(workspace, team)?;
|
|
20
|
-
let
|
|
21
|
-
|
|
21
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
22
|
+
workspace: &paths.run_workspace,
|
|
23
|
+
operation: "start-agent",
|
|
24
|
+
team,
|
|
25
|
+
agent_id: Some(agent_id),
|
|
26
|
+
})?;
|
|
27
|
+
let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
22
28
|
start_agent_at_paths(
|
|
23
29
|
&paths.run_workspace,
|
|
24
30
|
&paths.spec_workspace,
|
|
@@ -43,9 +49,23 @@ pub fn start_agent_with_transport(
|
|
|
43
49
|
team: Option<&str>,
|
|
44
50
|
transport: &dyn crate::transport::Transport,
|
|
45
51
|
) -> Result<StartAgentOutcome, LifecycleError> {
|
|
52
|
+
let paths = match lifecycle_paths(workspace, team) {
|
|
53
|
+
Ok(paths) => paths,
|
|
54
|
+
Err(_) if team.is_none() => LifecyclePaths {
|
|
55
|
+
run_workspace: workspace.to_path_buf(),
|
|
56
|
+
spec_workspace: workspace.to_path_buf(),
|
|
57
|
+
},
|
|
58
|
+
Err(error) => return Err(error),
|
|
59
|
+
};
|
|
60
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
61
|
+
workspace: &paths.run_workspace,
|
|
62
|
+
operation: "start-agent",
|
|
63
|
+
team,
|
|
64
|
+
agent_id: Some(agent_id),
|
|
65
|
+
})?;
|
|
46
66
|
start_agent_at_paths(
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
&paths.run_workspace,
|
|
68
|
+
&paths.spec_workspace,
|
|
49
69
|
agent_id,
|
|
50
70
|
force,
|
|
51
71
|
open_display,
|
|
@@ -74,24 +94,27 @@ pub(crate) fn start_agent_at_paths(
|
|
|
74
94
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?
|
|
75
95
|
};
|
|
76
96
|
crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
|
|
77
|
-
let
|
|
97
|
+
let raw_agent = state
|
|
78
98
|
.get("agents")
|
|
79
99
|
.and_then(|v| v.get(agent_id.as_str()))
|
|
80
100
|
.ok_or_else(|| LifecycleError::RequirementUnmet(format!("agent {agent_id} not found")))?
|
|
81
101
|
.clone();
|
|
82
|
-
|
|
102
|
+
let agent = rehydrate_agent_command_context_from_spec(spec_workspace, agent_id, &raw_agent);
|
|
103
|
+
if raw_agent
|
|
83
104
|
.get("paused")
|
|
84
105
|
.and_then(serde_json::Value::as_bool)
|
|
85
106
|
.unwrap_or(false)
|
|
86
107
|
{
|
|
87
|
-
return Ok(StartAgentOutcome::Paused {
|
|
108
|
+
return Ok(StartAgentOutcome::Paused {
|
|
109
|
+
agent_id: agent_id.clone(),
|
|
110
|
+
});
|
|
88
111
|
}
|
|
89
112
|
let session_name = state_session_name(&state);
|
|
90
113
|
let window = agent_window(&agent, agent_id);
|
|
91
114
|
let adaptive_layout =
|
|
92
115
|
open_display && crate::lifecycle::launch::state_uses_adaptive_layout(&state);
|
|
93
116
|
let agent_live = if adaptive_layout {
|
|
94
|
-
agent_pane_live(transport, &
|
|
117
|
+
agent_pane_live(transport, &raw_agent)
|
|
95
118
|
} else {
|
|
96
119
|
window_exists(transport, &session_name, &window)
|
|
97
120
|
};
|
|
@@ -103,7 +126,7 @@ pub(crate) fn start_agent_at_paths(
|
|
|
103
126
|
// spawn (defensive). The check itself remains a no-op on healthy
|
|
104
127
|
// state — assert_topology_invariants from Step 1 catches the
|
|
105
128
|
// upstream corruption.
|
|
106
|
-
let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &
|
|
129
|
+
let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &raw_agent);
|
|
107
130
|
if has_collision {
|
|
108
131
|
eprintln!(
|
|
109
132
|
"team_agent::layout e51_collision_post_step2 agent_id=`{agent_id}` \
|
|
@@ -116,7 +139,7 @@ pub(crate) fn start_agent_at_paths(
|
|
|
116
139
|
if !force && agent_live {
|
|
117
140
|
mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
|
|
118
141
|
let team_key = restart_projection_team_key(&state, team);
|
|
119
|
-
save_restart_projected_state(workspace, &mut state, &team_key)?;
|
|
142
|
+
save_restart_projected_state(workspace, &mut state, &team_key, &[agent_id.as_str()])?;
|
|
120
143
|
if let Ok(spec) = load_team_spec(spec_workspace) {
|
|
121
144
|
write_team_state(spec_workspace, &spec, &state)?;
|
|
122
145
|
}
|
|
@@ -208,7 +231,22 @@ pub(crate) fn start_agent_at_paths(
|
|
|
208
231
|
None,
|
|
209
232
|
Some(resolved_team_key.as_str()),
|
|
210
233
|
)?;
|
|
211
|
-
|
|
234
|
+
verify_spawned_pane_matches_target(
|
|
235
|
+
transport,
|
|
236
|
+
&spawn.spawn.pane_id,
|
|
237
|
+
&session_name,
|
|
238
|
+
&spawn.spawn.window,
|
|
239
|
+
)?;
|
|
240
|
+
let actual_spawn_window = spawn.spawn.window.as_str().to_string();
|
|
241
|
+
mark_agent_started(
|
|
242
|
+
&mut state,
|
|
243
|
+
agent_id,
|
|
244
|
+
&actual_spawn_window,
|
|
245
|
+
&spawn,
|
|
246
|
+
transport,
|
|
247
|
+
&safety,
|
|
248
|
+
start_mode,
|
|
249
|
+
)?;
|
|
212
250
|
// **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
|
|
213
251
|
// `state.tmux_socket` synchronized with the transport actually used for the
|
|
214
252
|
// spawn. Without this, add-agent / fork-agent could spawn to a socket that
|
|
@@ -218,24 +256,7 @@ pub(crate) fn start_agent_at_paths(
|
|
|
218
256
|
// after spawn" race with coordinator and no double source of truth.
|
|
219
257
|
crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
|
|
220
258
|
let team_key = restart_projection_team_key(&state, team);
|
|
221
|
-
save_restart_projected_state(workspace, &mut state, &team_key)?;
|
|
222
|
-
// **0.3.24 reachability gate (调整 4)** — strict, non-capture verification
|
|
223
|
-
// that the spawn actually produced an addressable window/pane on the
|
|
224
|
-
// transport's socket. Catches the macmini假绿 (`ok=true` returned by
|
|
225
|
-
// `add_agent` while the spawned `claude` process orphaned on a different
|
|
226
|
-
// socket and the leader never registered it). We use `has_pane` /
|
|
227
|
-
// `liveness` / `list_targets` (structural addressing only) — never
|
|
228
|
-
// `capture` — to avoid contention with E31's pane-readback gate timing.
|
|
229
|
-
if !spawned_pane_is_reachable(transport, &spawn.spawn.pane_id) {
|
|
230
|
-
return Err(LifecycleError::RequirementUnmet(format!(
|
|
231
|
-
"add-agent spawn unreachable: pane {} not addressable on transport \
|
|
232
|
-
socket {:?} (likely socket drift — see 0.3.24 fix); the agent \
|
|
233
|
-
process may have orphaned on a different tmux socket. Re-run after \
|
|
234
|
-
confirming the team's tmux_endpoint persisted via `team-agent status`.",
|
|
235
|
-
spawn.spawn.pane_id.as_str(),
|
|
236
|
-
transport.tmux_endpoint().unwrap_or_default()
|
|
237
|
-
)));
|
|
238
|
-
}
|
|
259
|
+
save_restart_projected_state(workspace, &mut state, &team_key, &[agent_id.as_str()])?;
|
|
239
260
|
write_start_agent_start_event(
|
|
240
261
|
workspace,
|
|
241
262
|
agent_id,
|
|
@@ -243,7 +264,7 @@ pub(crate) fn start_agent_at_paths(
|
|
|
243
264
|
provider,
|
|
244
265
|
start_mode,
|
|
245
266
|
&session_name,
|
|
246
|
-
&
|
|
267
|
+
&actual_spawn_window,
|
|
247
268
|
spawn_session_id,
|
|
248
269
|
tmux_start_mode_for_spawn(&spawn, into_existing_session),
|
|
249
270
|
)?;
|
|
@@ -262,40 +283,51 @@ pub(crate) fn start_agent_at_paths(
|
|
|
262
283
|
})
|
|
263
284
|
}
|
|
264
285
|
|
|
265
|
-
|
|
266
|
-
/// pane is currently addressable on the transport's tmux socket. Uses structural
|
|
267
|
-
/// addressing only (`has_pane` → `liveness` → `list_targets`) — **never**
|
|
268
|
-
/// `capture` — so it does not race with E31's pane-readback gate timing
|
|
269
|
-
/// (E31 reads pane contents after submit; running our own capture here would
|
|
270
|
-
/// either consume the first-paste tokens E31 looks for, or vice versa).
|
|
271
|
-
///
|
|
272
|
-
/// The gate fires after `mark_agent_started` + `save_restart_projected_state`,
|
|
273
|
-
/// so an unreachable pane returns a structured `LifecycleError` that the CLI
|
|
274
|
-
/// surface translates to `ok=false`, replacing the macmini假绿
|
|
275
|
-
/// (`ok=true` with orphaned-on-wrong-socket spawn).
|
|
276
|
-
fn spawned_pane_is_reachable(
|
|
286
|
+
fn verify_spawned_pane_matches_target(
|
|
277
287
|
transport: &dyn crate::transport::Transport,
|
|
278
288
|
pane: &crate::transport::PaneId,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
session: &crate::transport::SessionName,
|
|
290
|
+
window: &crate::transport::WindowName,
|
|
291
|
+
) -> Result<(), LifecycleError> {
|
|
292
|
+
let targets = transport
|
|
293
|
+
.list_targets()
|
|
294
|
+
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
295
|
+
let observed = targets.iter().find(|target| target.pane_id == *pane);
|
|
296
|
+
let Some(observed) = observed else {
|
|
297
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
298
|
+
"start refused: spawned pane not addressable on transport socket (possible socket drift); window disappeared or spawned pane not owned by requested agent window; requested={}:{} pane={} observed=<missing>",
|
|
299
|
+
session.as_str(),
|
|
300
|
+
window.as_str(),
|
|
301
|
+
pane.as_str()
|
|
302
|
+
)));
|
|
303
|
+
};
|
|
304
|
+
let observed_window = observed
|
|
305
|
+
.window_name
|
|
306
|
+
.as_ref()
|
|
307
|
+
.map(crate::transport::WindowName::as_str)
|
|
308
|
+
.unwrap_or("<unknown>");
|
|
309
|
+
if observed.session.as_str() != session.as_str() || observed_window != window.as_str() {
|
|
310
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
311
|
+
"start refused: spawned pane not owned by requested agent window; requested={}:{} pane={} observed={}:{}",
|
|
312
|
+
session.as_str(),
|
|
313
|
+
window.as_str(),
|
|
314
|
+
pane.as_str(),
|
|
315
|
+
observed.session.as_str(),
|
|
316
|
+
observed_window
|
|
317
|
+
)));
|
|
283
318
|
}
|
|
284
|
-
// (b) liveness probe: not-Dead → addressable. Dead → unreachable.
|
|
285
319
|
if matches!(
|
|
286
320
|
transport.liveness(pane),
|
|
287
|
-
Ok(crate::model::enums::PaneLiveness::
|
|
321
|
+
Ok(crate::model::enums::PaneLiveness::Dead)
|
|
288
322
|
) {
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
323
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
324
|
+
"start refused: spawned pane not addressable on transport socket; pane is dead after spawn; requested={}:{} pane={}",
|
|
325
|
+
session.as_str(),
|
|
326
|
+
window.as_str(),
|
|
327
|
+
pane.as_str()
|
|
328
|
+
)));
|
|
297
329
|
}
|
|
298
|
-
|
|
330
|
+
Ok(())
|
|
299
331
|
}
|
|
300
332
|
|
|
301
333
|
/// E51 (0.3.26 P0, restart self-heal): returns `true` when the agent's pane_id
|
|
@@ -318,9 +350,11 @@ fn pane_conflicts_with_leader_or_other(
|
|
|
318
350
|
let agent_socket = tmux_socket_field(agent).or(state_socket);
|
|
319
351
|
// Check leader anchor.
|
|
320
352
|
for key in ["leader_receiver", "team_owner"] {
|
|
321
|
-
if state
|
|
322
|
-
|
|
323
|
-
|
|
353
|
+
if state
|
|
354
|
+
.get(key)
|
|
355
|
+
.and_then(pane_socket_binding)
|
|
356
|
+
.is_some_and(|leader| pane_conflicts_on_same_socket(pane_id, agent_socket, leader))
|
|
357
|
+
{
|
|
324
358
|
return true;
|
|
325
359
|
}
|
|
326
360
|
}
|
|
@@ -392,10 +426,7 @@ fn pane_socket_binding(value: &serde_json::Value) -> Option<PaneSocketBinding<'_
|
|
|
392
426
|
})
|
|
393
427
|
}
|
|
394
428
|
|
|
395
|
-
fn agent_pane_live(
|
|
396
|
-
transport: &dyn crate::transport::Transport,
|
|
397
|
-
agent: &serde_json::Value,
|
|
398
|
-
) -> bool {
|
|
429
|
+
fn agent_pane_live(transport: &dyn crate::transport::Transport, agent: &serde_json::Value) -> bool {
|
|
399
430
|
let Some(pane) = agent
|
|
400
431
|
.get("pane_id")
|
|
401
432
|
.and_then(serde_json::Value::as_str)
|
|
@@ -458,8 +489,7 @@ fn list_same_role_panes(
|
|
|
458
489
|
/// caller falls back to safer behavior (refuse stop, surface
|
|
459
490
|
/// RequirementUnmet/transport error) when this returns false.
|
|
460
491
|
fn is_per_agent_window(window: &str, agent_id: &AgentId) -> bool {
|
|
461
|
-
window == agent_id.as_str()
|
|
462
|
-
&& !crate::lifecycle::launch::is_adaptive_layout_window_pub(window)
|
|
492
|
+
window == agent_id.as_str() && !crate::lifecycle::launch::is_adaptive_layout_window_pub(window)
|
|
463
493
|
}
|
|
464
494
|
|
|
465
495
|
fn tmux_start_mode_for_spawn(
|
|
@@ -584,8 +614,13 @@ pub fn stop_agent(
|
|
|
584
614
|
team: Option<&str>,
|
|
585
615
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
586
616
|
let paths = lifecycle_paths(workspace, team)?;
|
|
587
|
-
let
|
|
588
|
-
|
|
617
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
618
|
+
workspace: &paths.run_workspace,
|
|
619
|
+
operation: "stop-agent",
|
|
620
|
+
team,
|
|
621
|
+
agent_id: Some(agent_id),
|
|
622
|
+
})?;
|
|
623
|
+
let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
589
624
|
stop_agent_at_paths(
|
|
590
625
|
&paths.run_workspace,
|
|
591
626
|
&paths.spec_workspace,
|
|
@@ -602,6 +637,12 @@ pub fn stop_agent_with_transport(
|
|
|
602
637
|
transport: &dyn crate::transport::Transport,
|
|
603
638
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
604
639
|
let paths = lifecycle_paths(workspace, team)?;
|
|
640
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
641
|
+
workspace: &paths.run_workspace,
|
|
642
|
+
operation: "stop-agent",
|
|
643
|
+
team,
|
|
644
|
+
agent_id: Some(agent_id),
|
|
645
|
+
})?;
|
|
605
646
|
stop_agent_at_paths(
|
|
606
647
|
&paths.run_workspace,
|
|
607
648
|
&paths.spec_workspace,
|
|
@@ -622,8 +663,7 @@ pub(super) fn stop_agent_at_paths(
|
|
|
622
663
|
let mut state = resolve_team_scoped_state_or_refuse(workspace, team)?;
|
|
623
664
|
crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
|
|
624
665
|
let spec = load_team_spec(spec_workspace)?;
|
|
625
|
-
let agent = find_spec_agent(&spec, agent_id)
|
|
626
|
-
.ok_or_else(|| unknown_worker(agent_id))?;
|
|
666
|
+
let agent = find_spec_agent(&spec, agent_id).ok_or_else(|| unknown_worker(agent_id))?;
|
|
627
667
|
let session_name = state_session_name_from_spec(&state, &spec);
|
|
628
668
|
let window = state
|
|
629
669
|
.get("agents")
|
|
@@ -702,11 +742,15 @@ pub(super) fn stop_agent_at_paths(
|
|
|
702
742
|
};
|
|
703
743
|
if let Err(e) = kill_result {
|
|
704
744
|
let stderr = match &e {
|
|
705
|
-
crate::transport::TransportError::Subprocess { stderr, .. } =>
|
|
745
|
+
crate::transport::TransportError::Subprocess { stderr, .. } => {
|
|
746
|
+
stderr.trim().to_string()
|
|
747
|
+
}
|
|
706
748
|
other => other.to_string(),
|
|
707
749
|
};
|
|
708
750
|
let _ = write_stop_window_failed_event(workspace, agent_id, &target_str, &stderr);
|
|
709
|
-
return Err(LifecycleError::Transport(format!(
|
|
751
|
+
return Err(LifecycleError::Transport(format!(
|
|
752
|
+
"failed to stop agent {agent_id}: {stderr}"
|
|
753
|
+
)));
|
|
710
754
|
}
|
|
711
755
|
// 0.4.6 Stage 1: drain-and-prove. The pre-fix path trusted `kill_pane`'s
|
|
712
756
|
// success return without waiting for the pane / pid to actually exit.
|
|
@@ -729,8 +773,12 @@ pub(super) fn stop_agent_at_paths(
|
|
|
729
773
|
mark_agent_stopped(&mut state, agent_id, agent, &window)?;
|
|
730
774
|
// golden operations.py:95: save_team_scoped_state (team projection) — NOT a raw save, so a
|
|
731
775
|
// multi-team workspace keeps the other teams' persisted runtime state instead of being clobbered.
|
|
732
|
-
crate::state::projection::
|
|
733
|
-
|
|
776
|
+
crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority(
|
|
777
|
+
workspace,
|
|
778
|
+
&state,
|
|
779
|
+
&[agent_id.as_str()],
|
|
780
|
+
)
|
|
781
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
734
782
|
// golden operations.py:96-99: snapshot (side-effect), then state_file = write_team_state path.
|
|
735
783
|
// snapshot.py:19-21 returns None silently when session_name is falsy — mirror that no-op here so
|
|
736
784
|
// a workspace whose persisted state lacks session_name proceeds to write team_state and return ok
|
|
@@ -772,7 +820,9 @@ pub(super) fn resolve_team_scoped_state_or_refuse(
|
|
|
772
820
|
.unwrap_or("");
|
|
773
821
|
return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
|
|
774
822
|
}
|
|
775
|
-
state.ok_or_else(||
|
|
823
|
+
state.ok_or_else(|| {
|
|
824
|
+
LifecycleError::StatePersist("resolve_team_scoped_state returned no state".to_string())
|
|
825
|
+
})
|
|
776
826
|
}
|
|
777
827
|
|
|
778
828
|
fn mark_agent_started(
|
|
@@ -807,7 +857,10 @@ fn mark_agent_started(
|
|
|
807
857
|
// codex, claude, copilot. Reset_agent --discard-session already does
|
|
808
858
|
// this at common.rs:1144-1188; here we mirror it for start-agent /
|
|
809
859
|
// restart-agent fresh paths so the fresh-tuple invariant is global.
|
|
810
|
-
if matches!(
|
|
860
|
+
if matches!(
|
|
861
|
+
start_mode,
|
|
862
|
+
StartMode::Fresh | StartMode::FreshAfterMissingRollout
|
|
863
|
+
) {
|
|
811
864
|
for field in [
|
|
812
865
|
"session_id",
|
|
813
866
|
"rollout_path",
|
|
@@ -864,17 +917,10 @@ fn mark_agent_started(
|
|
|
864
917
|
// so future restart/start cycles read it directly from the agent row.
|
|
865
918
|
if let Some(ref team_id) = spawn.owner_team_id {
|
|
866
919
|
if !team_id.is_empty() {
|
|
867
|
-
agent.insert(
|
|
868
|
-
"owner_team_id".to_string(),
|
|
869
|
-
serde_json::json!(team_id),
|
|
870
|
-
);
|
|
920
|
+
agent.insert("owner_team_id".to_string(), serde_json::json!(team_id));
|
|
871
921
|
}
|
|
872
922
|
}
|
|
873
|
-
crate::lifecycle::launch::persist_command_plan_state(
|
|
874
|
-
agent,
|
|
875
|
-
&spawn.plan,
|
|
876
|
-
&spawn.profile_launch,
|
|
877
|
-
);
|
|
923
|
+
crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
|
|
878
924
|
crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
|
|
879
925
|
if let Some(placement) = spawn.layout_placement.as_ref() {
|
|
880
926
|
agent.insert(
|
|
@@ -885,7 +931,10 @@ fn mark_agent_started(
|
|
|
885
931
|
"layout_index".to_string(),
|
|
886
932
|
serde_json::json!(placement.layout_index),
|
|
887
933
|
);
|
|
888
|
-
agent.insert(
|
|
934
|
+
agent.insert(
|
|
935
|
+
"pane_index".to_string(),
|
|
936
|
+
serde_json::json!(placement.pane_index),
|
|
937
|
+
);
|
|
889
938
|
agent.insert(
|
|
890
939
|
"display".to_string(),
|
|
891
940
|
serde_json::json!({
|
|
@@ -926,8 +975,13 @@ pub fn reset_agent(
|
|
|
926
975
|
});
|
|
927
976
|
}
|
|
928
977
|
let paths = lifecycle_paths(workspace, team)?;
|
|
929
|
-
let
|
|
930
|
-
|
|
978
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
979
|
+
workspace: &paths.run_workspace,
|
|
980
|
+
operation: "reset-agent",
|
|
981
|
+
team,
|
|
982
|
+
agent_id: Some(agent_id),
|
|
983
|
+
})?;
|
|
984
|
+
let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
931
985
|
reset_agent_at_paths(
|
|
932
986
|
&paths.run_workspace,
|
|
933
987
|
&paths.spec_workspace,
|
|
@@ -953,6 +1007,12 @@ pub fn reset_agent_with_transport(
|
|
|
953
1007
|
});
|
|
954
1008
|
}
|
|
955
1009
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1010
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1011
|
+
workspace: &paths.run_workspace,
|
|
1012
|
+
operation: "reset-agent",
|
|
1013
|
+
team,
|
|
1014
|
+
agent_id: Some(agent_id),
|
|
1015
|
+
})?;
|
|
956
1016
|
reset_agent_at_paths(
|
|
957
1017
|
&paths.run_workspace,
|
|
958
1018
|
&paths.spec_workspace,
|
|
@@ -1160,12 +1220,12 @@ fn reset_agent_at_paths(
|
|
|
1160
1220
|
sync_restart_team_projections(&mut state, &team_key);
|
|
1161
1221
|
// golden operations.py (reset): save_team_scoped_state on the team projection — same multi-team
|
|
1162
1222
|
// preservation as stop, not a raw save_runtime_state.
|
|
1163
|
-
crate::state::projection::
|
|
1223
|
+
crate::state::projection::save_team_scoped_state_with_tombstone_lifecycle_topology_authority(
|
|
1164
1224
|
workspace,
|
|
1165
1225
|
&state,
|
|
1166
1226
|
&[agent_id.as_str()],
|
|
1167
1227
|
)
|
|
1168
|
-
|
|
1228
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1169
1229
|
// golden operations.py:125: write_team_state after the discard-save (the intermediate stopped snapshot).
|
|
1170
1230
|
write_team_state(spec_workspace, &spec, &state)?;
|
|
1171
1231
|
write_reset_tombstone_event(
|
|
@@ -1251,12 +1311,11 @@ fn write_start_agent_start_event(
|
|
|
1251
1311
|
// Contract C / F6.4: event log must record the same context-aware argv that the
|
|
1252
1312
|
// actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
|
|
1253
1313
|
let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
1254
|
-
let command_agent =
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
);
|
|
1314
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
1315
|
+
agent,
|
|
1316
|
+
Some(agent_id.as_str()),
|
|
1317
|
+
provider,
|
|
1318
|
+
);
|
|
1260
1319
|
let system_prompt =
|
|
1261
1320
|
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
1262
1321
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
@@ -1268,17 +1327,18 @@ fn write_start_agent_start_event(
|
|
|
1268
1327
|
let mcp_config = adapter
|
|
1269
1328
|
.mcp_config(auth_mode)
|
|
1270
1329
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
1271
|
-
let team_id = agent
|
|
1272
|
-
.get("owner_team_id")
|
|
1273
|
-
.and_then(|v| v.as_str());
|
|
1330
|
+
let team_id = agent.get("owner_team_id").and_then(|v| v.as_str());
|
|
1274
1331
|
let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
|
|
1275
1332
|
mcp_config,
|
|
1276
1333
|
workspace,
|
|
1277
1334
|
agent_id.as_str(),
|
|
1278
1335
|
team_id.unwrap_or(""),
|
|
1279
1336
|
);
|
|
1280
|
-
let mcp_config_path =
|
|
1281
|
-
|
|
1337
|
+
let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
|
|
1338
|
+
workspace,
|
|
1339
|
+
agent_id.as_str(),
|
|
1340
|
+
&mcp_config,
|
|
1341
|
+
)?;
|
|
1282
1342
|
let profile_launch =
|
|
1283
1343
|
crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
|
|
1284
1344
|
workspace,
|
|
@@ -1286,16 +1346,15 @@ fn write_start_agent_start_event(
|
|
|
1286
1346
|
agent,
|
|
1287
1347
|
Some(&mcp_config),
|
|
1288
1348
|
)?;
|
|
1289
|
-
let command_model = profile_launch
|
|
1290
|
-
.command_overrides
|
|
1291
|
-
.model
|
|
1292
|
-
.as_deref()
|
|
1293
|
-
.or(model);
|
|
1349
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
1294
1350
|
// 0.4.x provider effort MVP: start_agent path preserves effort from the
|
|
1295
1351
|
// persisted agent JSON.
|
|
1296
|
-
let start_agent_effort =
|
|
1352
|
+
let start_agent_effort =
|
|
1353
|
+
crate::lifecycle::launch::provider_effort_for_spawn_json(&agent, provider);
|
|
1297
1354
|
if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
|
|
1298
|
-
&agent,
|
|
1355
|
+
&agent,
|
|
1356
|
+
provider,
|
|
1357
|
+
agent_id.as_str(),
|
|
1299
1358
|
) {
|
|
1300
1359
|
let _ = crate::event_log::EventLog::new(workspace)
|
|
1301
1360
|
.write("provider.effort_unsupported", event_value);
|
|
@@ -1387,10 +1446,7 @@ pub(super) fn write_stop_drain_event(
|
|
|
1387
1446
|
}
|
|
1388
1447
|
}
|
|
1389
1448
|
crate::event_log::EventLog::new(workspace)
|
|
1390
|
-
.write(
|
|
1391
|
-
"stop_agent.drain",
|
|
1392
|
-
serde_json::Value::Object(payload),
|
|
1393
|
-
)
|
|
1449
|
+
.write("stop_agent.drain", serde_json::Value::Object(payload))
|
|
1394
1450
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1395
1451
|
Ok(())
|
|
1396
1452
|
}
|