@team-agent/installer 0.3.2 → 0.3.4

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.
Files changed (82) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +145 -11
  6. package/crates/team-agent/src/cli/emit.rs +287 -53
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +807 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler/tests.rs +2 -2
  16. package/crates/team-agent/src/compiler.rs +16 -6
  17. package/crates/team-agent/src/coordinator/health.rs +89 -20
  18. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  19. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  20. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  21. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  22. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  23. package/crates/team-agent/src/coordinator/types.rs +15 -3
  24. package/crates/team-agent/src/db/schema.rs +37 -2
  25. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  26. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  27. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  28. package/crates/team-agent/src/fake_worker.rs +146 -3
  29. package/crates/team-agent/src/leader/start.rs +121 -23
  30. package/crates/team-agent/src/leader/types.rs +44 -1
  31. package/crates/team-agent/src/lib.rs +3 -0
  32. package/crates/team-agent/src/lifecycle/display.rs +648 -50
  33. package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
  34. package/crates/team-agent/src/lifecycle/mod.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  36. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  37. package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
  38. package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
  40. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  41. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  42. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  45. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
  46. package/crates/team-agent/src/lifecycle/types.rs +23 -0
  47. package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
  48. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  50. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  51. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  52. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  53. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  54. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  55. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  56. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  57. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  58. package/crates/team-agent/src/message_store.rs +21 -4
  59. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  60. package/crates/team-agent/src/messaging/mod.rs +9 -6
  61. package/crates/team-agent/src/messaging/results.rs +153 -16
  62. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  63. package/crates/team-agent/src/messaging/send.rs +35 -3
  64. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  65. package/crates/team-agent/src/messaging/types.rs +11 -3
  66. package/crates/team-agent/src/os_probe.rs +119 -0
  67. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  68. package/crates/team-agent/src/packaging/tests.rs +23 -0
  69. package/crates/team-agent/src/provider/adapter.rs +483 -67
  70. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  71. package/crates/team-agent/src/provider/classify.rs +51 -4
  72. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  73. package/crates/team-agent/src/provider/types.rs +47 -0
  74. package/crates/team-agent/src/session_capture.rs +616 -0
  75. package/crates/team-agent/src/state/persist.rs +57 -0
  76. package/crates/team-agent/src/state/projection.rs +32 -23
  77. package/crates/team-agent/src/state/selector.rs +5 -2
  78. package/crates/team-agent/src/tmux_backend.rs +151 -60
  79. package/crates/team-agent/src/transport/test_support.rs +9 -0
  80. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  81. package/crates/team-agent/src/transport.rs +13 -2
  82. package/package.json +4 -4
@@ -75,7 +75,8 @@ pub(crate) fn start_agent_at_paths(
75
75
  let agent = state
76
76
  .get("agents")
77
77
  .and_then(|v| v.get(agent_id.as_str()))
78
- .ok_or_else(|| LifecycleError::RequirementUnmet(format!("agent {agent_id} not found")))?;
78
+ .ok_or_else(|| LifecycleError::RequirementUnmet(format!("agent {agent_id} not found")))?
79
+ .clone();
79
80
  if agent
80
81
  .get("paused")
81
82
  .and_then(serde_json::Value::as_bool)
@@ -84,7 +85,7 @@ pub(crate) fn start_agent_at_paths(
84
85
  return Ok(StartAgentOutcome::Paused { agent_id: agent_id.clone() });
85
86
  }
86
87
  let session_name = state_session_name(&state);
87
- let window = agent_window(agent, agent_id);
88
+ let window = agent_window(&agent, agent_id);
88
89
  if !force && window_exists(transport, &session_name, &window) {
89
90
  mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
90
91
  crate::state::projection::save_team_scoped_state(workspace, &state)
@@ -104,9 +105,9 @@ pub(crate) fn start_agent_at_paths(
104
105
  target,
105
106
  });
106
107
  }
107
- let provider = agent_provider(agent);
108
- let session_id = agent_session_id(agent);
109
- let rollout_path = agent_rollout_path(agent);
108
+ let provider = agent_provider(&agent);
109
+ let session_id = agent_session_id(&agent);
110
+ let rollout_path = agent_rollout_path(&agent);
110
111
  let rollout_exists = rollout_path
111
112
  .as_ref()
112
113
  .map(|p| p.as_path().exists())
@@ -125,20 +126,25 @@ pub(crate) fn start_agent_at_paths(
125
126
  };
126
127
  let into_existing_session =
127
128
  session_live_or_default(transport, &session_name, session_name_present(&state));
129
+ let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
128
130
  let spawn = spawn_agent_window(
129
131
  workspace,
130
132
  &session_name,
131
133
  agent_id,
132
- agent,
134
+ &agent,
133
135
  spawn_session_id,
134
136
  into_existing_session,
135
137
  transport,
138
+ Some(&safety),
136
139
  None,
137
140
  )?;
141
+ mark_agent_started(&mut state, agent_id, &window, &spawn, &safety)?;
142
+ crate::state::projection::save_team_scoped_state(workspace, &state)
143
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
138
144
  write_start_agent_start_event(
139
145
  workspace,
140
146
  agent_id,
141
- agent,
147
+ &agent,
142
148
  provider,
143
149
  start_mode,
144
150
  &session_name,
@@ -154,7 +160,7 @@ pub(crate) fn start_agent_at_paths(
154
160
  coordinator_started,
155
161
  },
156
162
  start_mode,
157
- target: spawn.pane_id.as_str().to_string(),
163
+ target: spawn.spawn.pane_id.as_str().to_string(),
158
164
  session_id,
159
165
  rollout_path,
160
166
  })
@@ -281,6 +287,43 @@ pub(super) fn resolve_team_scoped_state_or_refuse(
281
287
  state.ok_or_else(|| LifecycleError::StatePersist("resolve_team_scoped_state returned no state".to_string()))
282
288
  }
283
289
 
290
+ fn mark_agent_started(
291
+ state: &mut serde_json::Value,
292
+ agent_id: &AgentId,
293
+ window: &str,
294
+ spawn: &SpawnedAgentWindow,
295
+ safety: &DangerousApproval,
296
+ ) -> Result<(), LifecycleError> {
297
+ let Some(agent) = state
298
+ .get_mut("agents")
299
+ .and_then(serde_json::Value::as_object_mut)
300
+ .and_then(|agents| agents.get_mut(agent_id.as_str()))
301
+ .and_then(serde_json::Value::as_object_mut)
302
+ else {
303
+ return Err(LifecycleError::StatePersist(format!(
304
+ "agent {} state is not an object",
305
+ agent_id
306
+ )));
307
+ };
308
+ agent.insert("status".to_string(), serde_json::json!("running"));
309
+ agent.insert("agent_id".to_string(), serde_json::json!(agent_id.as_str()));
310
+ agent.insert("window".to_string(), serde_json::json!(window));
311
+ agent.insert(
312
+ "pane_id".to_string(),
313
+ serde_json::json!(spawn.spawn.pane_id.as_str()),
314
+ );
315
+ if let Some(pane_pid) = spawn.spawn.child_pid {
316
+ agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
317
+ }
318
+ crate::lifecycle::launch::persist_command_plan_state(
319
+ agent,
320
+ &spawn.plan,
321
+ &spawn.profile_launch,
322
+ );
323
+ crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
324
+ Ok(())
325
+ }
326
+
284
327
  /// `reset_agent(workspace, agent_id, discard_session, open_display, team)`
285
328
  /// (`lifecycle/operations.py:102`)。discard + 重起;**未传 discard_session → 拒绝**。
286
329
  pub fn reset_agent(
@@ -408,32 +451,76 @@ fn write_start_agent_start_event(
408
451
  let adapter = crate::provider::get_adapter(provider);
409
452
  // Contract C / F6.4: event log must record the same context-aware argv that the
410
453
  // actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
411
- let role = agent.get("role").and_then(|v| v.as_str());
412
454
  let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
413
- let tools = crate::lifecycle::launch::worker_tool_refs(agent_tool_strings(agent), &safety);
414
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
455
+ let command_agent =
456
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
457
+ agent,
458
+ Some(agent_id.as_str()),
459
+ provider,
460
+ );
461
+ let system_prompt =
462
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
463
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
464
+ &command_agent,
465
+ provider,
466
+ &safety,
467
+ )?;
468
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
415
469
  let mcp_config = adapter
416
470
  .mcp_config(auth_mode)
417
471
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
418
- let mut argv = match session_id {
472
+ let team_id = agent
473
+ .get("owner_team_id")
474
+ .and_then(|v| v.as_str());
475
+ let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
476
+ mcp_config,
477
+ workspace,
478
+ agent_id.as_str(),
479
+ team_id.unwrap_or(""),
480
+ );
481
+ let mcp_config_path =
482
+ crate::lifecycle::launch::write_worker_mcp_config(workspace, agent_id.as_str(), &mcp_config)?;
483
+ let profile_launch =
484
+ crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
485
+ workspace,
486
+ agent_id.as_str(),
487
+ agent,
488
+ Some(&mcp_config),
489
+ )?;
490
+ let command_model = profile_launch
491
+ .command_overrides
492
+ .model
493
+ .as_deref()
494
+ .or(model);
495
+ let context = crate::provider::ProviderCommandContext {
496
+ auth_mode,
497
+ mcp_config: Some(&mcp_config),
498
+ system_prompt: Some(system_prompt.as_str()),
499
+ model: command_model,
500
+ tools: &resolved_tool_refs,
501
+ profile_launch: Some(&profile_launch),
502
+ };
503
+ let mut plan = match session_id {
419
504
  Some(session_id) => adapter
420
- .build_resume_command_with_context(
421
- Some(session_id),
422
- auth_mode,
423
- Some(&mcp_config),
424
- role,
425
- model,
426
- &tool_refs,
427
- )
505
+ .build_resume_command_plan(Some(session_id), context)
428
506
  .map_err(|e| LifecycleError::Provider(e.to_string()))?,
429
507
  None => adapter
430
- .build_command_with_tools(auth_mode, Some(&mcp_config), role, model, &tool_refs)
508
+ .build_command_plan(context)
431
509
  .map_err(|e| LifecycleError::Provider(e.to_string()))?,
432
510
  };
433
- let team_id = agent
434
- .get("owner_team_id")
435
- .and_then(|v| v.as_str());
436
- crate::lifecycle::launch::fill_spawn_placeholders_full(&mut argv, workspace, agent_id.as_str(), team_id);
511
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
512
+ crate::lifecycle::launch::point_native_mcp_config_at_file(
513
+ &mut plan.argv,
514
+ provider,
515
+ &mcp_config_path,
516
+ );
517
+ }
518
+ crate::lifecycle::launch::fill_spawn_placeholders_full(
519
+ &mut plan.argv,
520
+ workspace,
521
+ agent_id.as_str(),
522
+ team_id,
523
+ );
437
524
  let tmux_start_mode = if into_existing_session {
438
525
  "new-window"
439
526
  } else {
@@ -450,7 +537,7 @@ fn write_start_agent_start_event(
450
537
  "session": session_name.as_str(),
451
538
  "window": window,
452
539
  "tmux_start_mode": tmux_start_mode,
453
- "command": argv,
540
+ "command": plan.argv,
454
541
  "mcp_config": agent.get("mcp_config").cloned().unwrap_or(serde_json::Value::Null),
455
542
  }),
456
543
  )
@@ -1,5 +1,11 @@
1
1
  use super::*;
2
2
 
3
+ pub(super) struct SpawnedAgentWindow {
4
+ pub spawn: crate::transport::SpawnResult,
5
+ pub plan: crate::provider::CommandPlan,
6
+ pub profile_launch: crate::provider::ProviderProfileLaunch,
7
+ }
8
+
3
9
  pub(super) fn spawn_agent_window(
4
10
  workspace: &Path,
5
11
  session_name: &SessionName,
@@ -9,7 +15,8 @@ pub(super) fn spawn_agent_window(
9
15
  into_existing_session: bool,
10
16
  transport: &dyn crate::transport::Transport,
11
17
  safety: Option<&DangerousApproval>,
12
- ) -> Result<crate::transport::SpawnResult, LifecycleError> {
18
+ spawn_cwd_override: Option<&Path>,
19
+ ) -> Result<SpawnedAgentWindow, LifecycleError> {
13
20
  let provider = agent_provider(agent);
14
21
  let auth_mode = agent_auth_mode(agent);
15
22
  let model = agent.get("model").and_then(|v| v.as_str());
@@ -22,7 +29,6 @@ pub(super) fn spawn_agent_window(
22
29
  // Contract C / F6.4: thread compiled role/tools/MCP context through restart as well —
23
30
  // a restarted worker must come back up with the SAME callable MCP capability + role
24
31
  // prompt as a fresh launch, else `report_result` becomes unreachable after every restart.
25
- let role = agent.get("role").and_then(|v| v.as_str());
26
32
  let detected_safety;
27
33
  let safety = if let Some(safety) = safety {
28
34
  safety
@@ -30,26 +36,20 @@ pub(super) fn spawn_agent_window(
30
36
  detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
31
37
  &detected_safety
32
38
  };
33
- let tools = crate::lifecycle::launch::worker_tool_refs(agent_tool_strings(agent), safety);
34
- let tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
35
- let mcp_config = adapter
36
- .mcp_config(auth_mode)
37
- .map_err(|e| LifecycleError::Provider(e.to_string()))?;
38
- let mut argv = match resume_session_id {
39
- Some(session_id) => adapter
40
- .build_resume_command_with_context(
41
- Some(session_id),
42
- auth_mode,
43
- Some(&mcp_config),
44
- role,
45
- model,
46
- &tool_refs,
47
- )
48
- .map_err(|e| LifecycleError::Provider(e.to_string()))?,
49
- None => adapter
50
- .build_command_with_tools(auth_mode, Some(&mcp_config), role, model, &tool_refs)
51
- .map_err(|e| LifecycleError::Provider(e.to_string()))?,
52
- };
39
+ let command_agent =
40
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
41
+ agent,
42
+ Some(agent_id.as_str()),
43
+ provider,
44
+ );
45
+ let system_prompt =
46
+ crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
47
+ let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
48
+ &command_agent,
49
+ provider,
50
+ safety,
51
+ )?;
52
+ let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
53
53
  // owner_team_id resolution: prefer the runtime-state row's `owner_team_id` (set by
54
54
  // launch/restart); fall back to the active team key for paths that don't write the
55
55
  // row first (e.g. add-agent calls spawn before upserting team metadata).
@@ -63,22 +63,78 @@ pub(super) fn spawn_agent_window(
63
63
  let key = crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
64
64
  (!key.is_empty()).then_some(key)
65
65
  });
66
+ let mcp_config = adapter
67
+ .mcp_config(auth_mode)
68
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?;
69
+ let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
70
+ mcp_config,
71
+ workspace,
72
+ agent_id.as_str(),
73
+ team_id.as_deref().unwrap_or(""),
74
+ );
75
+ let mcp_config_path =
76
+ crate::lifecycle::launch::write_worker_mcp_config(workspace, agent_id.as_str(), &mcp_config)?;
77
+ let profile_launch =
78
+ crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
79
+ workspace,
80
+ agent_id.as_str(),
81
+ agent,
82
+ Some(&mcp_config),
83
+ )?;
84
+ let command_model = profile_launch
85
+ .command_overrides
86
+ .model
87
+ .as_deref()
88
+ .or(model);
89
+ let context = crate::provider::ProviderCommandContext {
90
+ auth_mode,
91
+ mcp_config: Some(&mcp_config),
92
+ system_prompt: Some(system_prompt.as_str()),
93
+ model: command_model,
94
+ tools: &resolved_tool_refs,
95
+ profile_launch: Some(&profile_launch),
96
+ };
97
+ let mut plan = match resume_session_id {
98
+ Some(session_id) => adapter
99
+ .build_resume_command_plan(Some(session_id), context)
100
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?,
101
+ None => adapter
102
+ .build_command_plan(context)
103
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?,
104
+ };
105
+ if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
106
+ crate::lifecycle::launch::point_native_mcp_config_at_file(
107
+ &mut plan.argv,
108
+ provider,
109
+ &mcp_config_path,
110
+ );
111
+ }
66
112
  crate::lifecycle::launch::fill_spawn_placeholders_full(
67
- &mut argv,
113
+ &mut plan.argv,
68
114
  workspace,
69
115
  agent_id.as_str(),
70
116
  team_id.as_deref(),
71
117
  );
72
118
  let window = WindowName::new(agent_id.as_str());
73
- let env = crate::lifecycle::launch::inherited_env_with_team_overrides(
119
+ let mut env = crate::lifecycle::launch::inherited_env_with_team_overrides(
74
120
  workspace,
75
121
  agent_id.as_str(),
76
122
  team_id.as_deref(),
77
123
  );
124
+ crate::lifecycle::launch::apply_profile_launch_env(&mut env, &profile_launch);
125
+ let spawn_cwd = spawn_cwd_override
126
+ .or_else(|| {
127
+ agent
128
+ .get("spawn_cwd")
129
+ .and_then(|v| v.as_str())
130
+ .filter(|cwd| !cwd.is_empty())
131
+ .map(Path::new)
132
+ })
133
+ .unwrap_or(workspace);
78
134
  let result = if into_existing_session {
79
- transport.spawn_into(session_name, &window, &argv, workspace, &env)
135
+ transport.spawn_into(session_name, &window, &plan.argv, spawn_cwd, &env)
80
136
  } else {
81
- transport.spawn_first(session_name, &window, &argv, workspace, &env)
137
+ transport.spawn_first(session_name, &window, &plan.argv, spawn_cwd, &env)
82
138
  };
83
139
  let spawn = result.map_err(|e| LifecycleError::Transport(e.to_string()))?;
84
140
  let _ = adapter.handle_startup_prompts(
@@ -87,7 +143,11 @@ pub(super) fn spawn_agent_window(
87
143
  30,
88
144
  0.5,
89
145
  );
90
- Ok(spawn)
146
+ Ok(SpawnedAgentWindow {
147
+ spawn,
148
+ plan,
149
+ profile_launch,
150
+ })
91
151
  }
92
152
 
93
153
  pub(super) fn start_coordinator_for_workspace(workspace: &Path) -> Result<bool, LifecycleError> {
@@ -97,6 +157,13 @@ pub(super) fn start_coordinator_for_workspace(workspace: &Path) -> Result<bool,
97
157
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
98
158
  }
99
159
 
160
+ pub(super) fn persist_effective_approval_policy_for_restart(
161
+ agent: &mut serde_json::Map<String, serde_json::Value>,
162
+ safety: &DangerousApproval,
163
+ ) {
164
+ crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
165
+ }
166
+
100
167
  pub(super) fn state_session_name(state: &serde_json::Value) -> SessionName {
101
168
  state
102
169
  .get("session_name")
@@ -163,86 +230,106 @@ pub(super) fn agent_rollout_path(agent: &serde_json::Value) -> Option<RolloutPat
163
230
  pub(crate) fn refresh_missing_provider_sessions(
164
231
  state: &mut serde_json::Value,
165
232
  ) -> Result<bool, LifecycleError> {
166
- let Some(agents) = state.get_mut("agents").and_then(serde_json::Value::as_object_mut) else {
167
- return Ok(false);
168
- };
169
- let mut changed = false;
170
- for (agent_id, agent) in agents {
171
- let Some(agent_obj) = agent.as_object_mut() else {
172
- continue;
173
- };
174
- if agent_obj
175
- .get("session_id")
176
- .and_then(serde_json::Value::as_str)
177
- .is_some_and(|session| !session.is_empty())
178
- {
179
- continue;
180
- }
181
- let Some(spawn_cwd) = agent_obj
182
- .get("spawn_cwd")
183
- .and_then(serde_json::Value::as_str)
184
- .filter(|cwd| !cwd.is_empty())
185
- else {
186
- continue;
187
- };
188
- let provider = agent_provider(&serde_json::Value::Object(agent_obj.clone()));
189
- let adapter = crate::provider::get_adapter(provider);
190
- let captured = adapter
191
- .capture_session_id(agent_id, Path::new(spawn_cwd), 0)
192
- .map_err(|e| LifecycleError::Provider(e.to_string()))?;
193
- let Some(captured) = captured else {
194
- continue;
195
- };
196
- if let Some(session_id) = captured.session_id {
197
- agent_obj.insert(
198
- "session_id".to_string(),
199
- serde_json::json!(session_id.as_str()),
200
- );
201
- changed = true;
202
- }
203
- if let Some(rollout_path) = captured.rollout_path {
204
- agent_obj.insert(
205
- "rollout_path".to_string(),
206
- serde_json::json!(rollout_path.as_path().to_string_lossy()),
207
- );
208
- changed = true;
209
- }
210
- agent_obj.insert(
211
- "captured_at".to_string(),
212
- serde_json::json!(chrono::Utc::now().to_rfc3339()),
213
- );
214
- agent_obj.insert(
215
- "captured_via".to_string(),
216
- serde_json::to_value(captured.captured_via)
217
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
218
- );
219
- agent_obj.insert(
220
- "attribution_confidence".to_string(),
221
- serde_json::to_value(captured.attribution_confidence)
222
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
223
- );
233
+ crate::session_capture::capture_missing_provider_sessions_once(
234
+ state,
235
+ &mut crate::provider::get_adapter,
236
+ false,
237
+ 0,
238
+ )
239
+ .map(|report| report.changed)
240
+ .map_err(|e| LifecycleError::Provider(e.to_string()))
241
+ }
242
+
243
+ pub(crate) fn converge_missing_provider_sessions(
244
+ state: &mut serde_json::Value,
245
+ deadline: std::time::Duration,
246
+ poll_interval: std::time::Duration,
247
+ workspace: &Path,
248
+ allow_fresh: bool,
249
+ ) -> Result<crate::session_capture::SessionConvergence, LifecycleError> {
250
+ crate::session_capture::converge_missing_provider_sessions(
251
+ state,
252
+ &mut crate::provider::get_adapter,
253
+ deadline,
254
+ poll_interval,
255
+ restart_required_missing_session_agent_ids,
256
+ |progress| {
257
+ let pending_agent_ids = progress.pending_agent_ids.clone();
258
+ write_session_convergence_progress_event(
259
+ workspace,
260
+ serde_json::json!({
261
+ "ts": chrono::Utc::now().to_rfc3339(),
262
+ "event": "provider.session.converging",
263
+ "iteration": progress.iteration,
264
+ "elapsed_ms": progress.elapsed_ms,
265
+ "deadline_ms": progress.deadline_ms,
266
+ "changed": progress.changed,
267
+ "assigned": progress.assigned,
268
+ "missing": progress.missing,
269
+ "required_missing": progress.required_missing_agent_ids.clone(),
270
+ "required_missing_agent_ids": progress.required_missing_agent_ids,
271
+ "pending": pending_agent_ids,
272
+ "pending_agent_ids": progress.pending_agent_ids,
273
+ "candidate_count_by_agent": progress.candidate_count_by_agent,
274
+ "remaining_ms": progress.remaining_ms,
275
+ "allow_fresh": allow_fresh,
276
+ }),
277
+ )
278
+ },
279
+ )
280
+ .map_err(LifecycleError::StatePersist)
281
+ }
282
+
283
+ fn write_session_convergence_progress_event(
284
+ workspace: &Path,
285
+ event: serde_json::Value,
286
+ ) -> Result<(), String> {
287
+ use std::io::Write as _;
288
+
289
+ let path = workspace.join(".team").join("logs").join("events.jsonl");
290
+ if let Some(parent) = path.parent() {
291
+ std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
224
292
  }
225
- Ok(changed)
293
+ let line = serde_json::to_string(&event).map_err(|e| e.to_string())?;
294
+ let mut file = std::fs::OpenOptions::new()
295
+ .create(true)
296
+ .append(true)
297
+ .open(path)
298
+ .map_err(|e| e.to_string())?;
299
+ file.write_all(line.as_bytes())
300
+ .and_then(|_| file.write_all(b"\n"))
301
+ .map_err(|e| e.to_string())
226
302
  }
227
303
 
228
- /// Tools list off an agent's runtime state entry (`tools: [...]`). Restart paths
229
- /// don't have the full spec object, only the runtime state — so they read tools from
230
- /// the state row, falling back to an empty list. Contract C requires the worker
231
- /// command be built with the tool list, even on restart.
232
- pub(super) fn agent_tool_strings(agent: &serde_json::Value) -> Vec<String> {
233
- agent
234
- .get("tools")
235
- .and_then(|v| v.as_array())
236
- .map(|items| {
237
- items
238
- .iter()
239
- .filter_map(|v| v.as_str())
240
- .map(str::to_string)
241
- .collect()
304
+ pub(crate) fn restart_required_missing_session_agent_ids(state: &serde_json::Value) -> Vec<String> {
305
+ let mut missing = crate::session_capture::incomplete_resumable_agent_ids(state)
306
+ .into_iter()
307
+ .filter(|agent_id| {
308
+ let Some(agent) = state.get("agents").and_then(|agents| agents.get(agent_id)) else {
309
+ return false;
310
+ };
311
+ let missing_session_id = agent
312
+ .get("session_id")
313
+ .and_then(|value| value.as_str())
314
+ .is_none_or(|session| session.is_empty());
315
+ let is_running = agent
316
+ .get("status")
317
+ .and_then(|value| value.as_str())
318
+ .is_some_and(|status| status == "running");
319
+ let has_live_pane_binding = agent
320
+ .get("pane_id")
321
+ .and_then(|value| value.as_str())
322
+ .is_some_and(|pane| !pane.is_empty());
323
+ let has_interaction_marker = agent
324
+ .get("first_send_at")
325
+ .and_then(|value| value.as_str())
326
+ .is_some_and(|value| !value.is_empty());
327
+ missing_session_id && is_running && (has_live_pane_binding || has_interaction_marker)
242
328
  })
243
- .unwrap_or_default()
329
+ .collect::<Vec<_>>();
330
+ missing.sort();
331
+ missing
244
332
  }
245
-
246
333
  pub(super) fn agent_window(agent: &serde_json::Value, agent_id: &AgentId) -> String {
247
334
  agent
248
335
  .get("window")