@team-agent/installer 0.3.1 → 0.3.3

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 (79) 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 +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  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 +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -0,0 +1,58 @@
1
+ //! Shared coordinator runtime observation seam.
2
+ //!
3
+ //! S0 only defines the typed capture/result surface. Lane 1 fills capture facts;
4
+ //! Lane 2 fills detector results from those facts.
5
+
6
+ use std::collections::BTreeMap;
7
+ use std::path::Path;
8
+
9
+ use serde_json::Value;
10
+
11
+ use crate::model::enums::Provider;
12
+ use crate::model::ids::{AgentId, TeamKey};
13
+ use crate::provider::{ProcessLiveness, RolloutPath};
14
+ use crate::transport::{PaneId, PaneInfo, SessionName, WindowName};
15
+
16
+ use super::types::{CompactionResult, LeaderApiError, SessionDriftResult};
17
+
18
+ #[derive(Debug, Clone, PartialEq, Eq)]
19
+ pub struct CapturedRuntimeFact {
20
+ pub team_key: Option<TeamKey>,
21
+ pub agent_id: AgentId,
22
+ pub provider: Option<Provider>,
23
+ pub session_name: Option<SessionName>,
24
+ pub window: Option<WindowName>,
25
+ pub pane_id: Option<PaneId>,
26
+ pub scrollback_tail: String,
27
+ pub pane_info: Option<PaneInfo>,
28
+ pub agent_state_snapshot: Value,
29
+ pub stored_session_id: Option<String>,
30
+ pub last_output_at: Option<String>,
31
+ pub rollout_path: Option<RolloutPath>,
32
+ pub process_liveness: Option<ProcessLiveness>,
33
+ }
34
+
35
+ #[derive(Debug, Clone, PartialEq, Eq)]
36
+ pub struct LeaderCaptureFact {
37
+ pub team_key: Option<TeamKey>,
38
+ pub leader_receiver: Option<Value>,
39
+ pub pane_id: Option<PaneId>,
40
+ pub scrollback_tail: String,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
44
+ pub struct RuntimeObservationResults {
45
+ pub captures_by_agent: BTreeMap<AgentId, CapturedRuntimeFact>,
46
+ pub compaction: Vec<CompactionResult>,
47
+ pub session_drift: Vec<SessionDriftResult>,
48
+ pub api_errors: Vec<LeaderApiError>,
49
+ }
50
+
51
+ pub fn observe(
52
+ workspace: &Path,
53
+ state: &mut Value,
54
+ captures_by_agent: BTreeMap<AgentId, CapturedRuntimeFact>,
55
+ leader_capture: Option<LeaderCaptureFact>,
56
+ ) -> RuntimeObservationResults {
57
+ super::runtime_detectors::observe_runtime(workspace, state, captures_by_agent, leader_capture)
58
+ }
@@ -1,5 +1,6 @@
1
1
  //! Coordinator core:daemon lifecycle 宿主 + 单次 tick 编排(19 步固定顺序)+ health/start/stop。
2
2
 
3
+ use std::collections::BTreeMap;
3
4
  use std::path::{Path, PathBuf};
4
5
 
5
6
  use serde_json::Value;
@@ -17,6 +18,7 @@ use super::health::{
17
18
  coordinator_log_path, coordinator_meta_path, coordinator_metadata_ok, coordinator_pid_path,
18
19
  pid_is_running, read_coordinator_metadata, write_coordinator_metadata,
19
20
  };
21
+ use super::runtime_observation::{self, CapturedRuntimeFact};
20
22
  use super::types::{
21
23
  AgentId, CoordinatorHealthStatus, HealthReport, MetadataSource, Pid, ProviderRegistry,
22
24
  SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome, StopReport,
@@ -206,7 +208,7 @@ impl Coordinator {
206
208
  }
207
209
 
208
210
  self.record_step("capture_missing");
209
- self.capture_missing_sessions(&mut state)?;
211
+ self.capture_missing_sessions(&mut state, &event_log)?;
210
212
 
211
213
  self.record_step("refresh_statuses");
212
214
  // TODO(spine slice 2b): split lightweight runtime status refresh from health sync.
@@ -230,7 +232,7 @@ impl Coordinator {
230
232
  self.handle_runtime_approval_prompts(&mut state, &event_log)?;
231
233
 
232
234
  self.record_step("sync_health");
233
- self.sync_agent_health(&mut state, &store, &event_log)?;
235
+ let captures_by_agent = self.sync_agent_health(&mut state, &store, &event_log)?;
234
236
  self.detect_abnormal_exits(&mut state, &event_log)?;
235
237
 
236
238
  self.record_step("deliver_pending");
@@ -269,17 +271,34 @@ impl Coordinator {
269
271
  let idle_alerts: Vec<IdleAlert> = Vec::new();
270
272
  self.record_step("detect_deadlocks");
271
273
  let deadlock_alerts: Vec<DeadlockAlert> = Vec::new();
272
- let _ = (&state, &store);
274
+ let _ = &store;
273
275
 
274
- for step in ["detect_compaction", "detect_drift", "detect_api_errors"] {
275
- self.record_step(step);
276
- // TODO(spine slice 2): wire via capture seam.
277
- }
276
+ self.record_step("detect_compaction");
277
+ self.record_step("detect_drift");
278
+ self.record_step("detect_api_errors");
279
+ let leader_capture = self.capture_leader_receiver(&state);
280
+ let observations = runtime_observation::observe(
281
+ self.workspace.as_path(),
282
+ &mut state,
283
+ captures_by_agent,
284
+ leader_capture,
285
+ );
286
+ let mut collections = TickCollections {
287
+ delivered,
288
+ scheduled,
289
+ stuck,
290
+ idle_alerts,
291
+ deadlock_alerts,
292
+ compaction: observations.compaction,
293
+ session_drift: observations.session_drift,
294
+ api_errors: observations.api_errors,
295
+ results: Vec::new(),
296
+ };
278
297
 
279
298
  self.record_step("atomic_save");
280
299
  let saved = match &self.save_hook {
281
300
  Some(hook) => hook(&self.workspace, &state),
282
- None => crate::state::persist::save_runtime_state(self.workspace.as_path(), &state),
301
+ None => crate::state::projection::save_team_scoped_state(self.workspace.as_path(), &state),
283
302
  };
284
303
  if saved.is_err() {
285
304
  return Ok(base_tick_report(
@@ -287,19 +306,12 @@ impl Coordinator {
287
306
  false,
288
307
  Some(TickStopReason::PersistenceDegraded),
289
308
  Some(false),
290
- TickCollections {
291
- delivered,
292
- scheduled,
293
- stuck,
294
- idle_alerts,
295
- deadlock_alerts,
296
- results: Vec::new(),
297
- },
309
+ collections,
298
310
  ));
299
311
  }
300
312
 
301
313
  self.record_step("collect_results");
302
- let results = collect_results(
314
+ collections.results = collect_results(
303
315
  crate::messaging::collect_results_and_notify_watchers(self.workspace.as_path(), &event_log)?,
304
316
  );
305
317
  self.record_step("prune_dedupe_log");
@@ -308,7 +320,7 @@ impl Coordinator {
308
320
  false,
309
321
  None,
310
322
  Some(true),
311
- TickCollections { delivered, scheduled, stuck, idle_alerts, deadlock_alerts, results },
323
+ collections,
312
324
  ))
313
325
  }
314
326
 
@@ -317,44 +329,21 @@ impl Coordinator {
317
329
  // were removed by design. Delivery primitives still flow through the rest of
318
330
  // the tick body unchanged.
319
331
 
320
- fn capture_missing_sessions(&self, state: &mut Value) -> Result<(), TickError> {
321
- let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
322
- return Ok(());
323
- };
324
- for (agent_id, agent) in agents {
325
- let Some(agent_obj) = agent.as_object_mut() else {
326
- continue;
327
- };
328
- if agent_obj.get("session_id").and_then(Value::as_str).is_some() {
329
- continue;
330
- }
331
- let Some(spawn_cwd) = agent_obj.get("spawn_cwd").and_then(Value::as_str) else {
332
- continue;
333
- };
334
- let Some(provider) = agent_obj
335
- .get("provider")
336
- .and_then(Value::as_str)
337
- .and_then(parse_provider)
338
- else {
339
- continue;
340
- };
341
- let adapter = self.provider_registry.adapter_for(provider);
342
- let captured = adapter.capture_session_id(
343
- agent_id,
344
- std::path::Path::new(spawn_cwd),
345
- 0,
332
+ fn capture_missing_sessions(&self, state: &mut Value, event_log: &EventLog) -> Result<(), TickError> {
333
+ let report = crate::session_capture::capture_missing_provider_sessions_once(
334
+ state,
335
+ &mut |provider| self.provider_registry.adapter_for(provider),
336
+ true,
337
+ 0,
338
+ )?;
339
+ for ambiguous in report.ambiguous {
340
+ event_log.write(
341
+ "provider.session.attribution_ambiguous",
342
+ serde_json::json!({
343
+ "agent_id": ambiguous.agent_id,
344
+ "spawn_cwd": ambiguous.spawn_cwd,
345
+ }),
346
346
  )?;
347
- if let Some(captured) = captured {
348
- if let Some(session_id) = captured.session_id {
349
- agent_obj.insert("session_id".to_string(), serde_json::json!(session_id.as_str()));
350
- }
351
- if let Some(rollout_path) = captured.rollout_path {
352
- agent_obj.insert(
353
- "rollout_path".to_string(),
354
- serde_json::json!(rollout_path.as_path().to_string_lossy()),
355
- );
356
- }
357
- }
358
347
  }
359
348
  Ok(())
360
349
  }
@@ -364,12 +353,15 @@ impl Coordinator {
364
353
  state: &mut Value,
365
354
  store: &crate::message_store::MessageStore,
366
355
  event_log: &EventLog,
367
- ) -> Result<(), TickError> {
356
+ ) -> Result<BTreeMap<AgentId, CapturedRuntimeFact>, TickError> {
357
+ let mut captures = BTreeMap::new();
368
358
  let snapshot = state.clone();
369
359
  let team = crate::state::projection::team_state_key(&snapshot);
360
+ let team_key = Some(crate::model::ids::TeamKey::new(team.clone()));
370
361
  let session_name = state.get("session_name").and_then(Value::as_str).map(str::to_string);
362
+ let pane_infos = self.transport.list_targets().unwrap_or_default();
371
363
  let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
372
- return Ok(());
364
+ return Ok(captures);
373
365
  };
374
366
  for (agent_id, agent) in agents {
375
367
  let Some((session, window, target)) = capture_window_target(agent, session_name.as_deref()) else {
@@ -417,18 +409,72 @@ impl Coordinator {
417
409
  .get("pane_current_command")
418
410
  .or_else(|| agent.get("current_command"))
419
411
  .and_then(Value::as_str);
420
- let last_output_at = agent.get("last_output_at").and_then(Value::as_str);
412
+ let last_output_at_before = agent.get("last_output_at").and_then(Value::as_str);
421
413
  let activity = crate::messaging::classify_agent_activity(
422
414
  &snapshot,
423
415
  &captured.text,
424
416
  pane_in_mode,
425
417
  current_command,
426
- last_output_at,
418
+ last_output_at_before,
427
419
  );
428
420
  let last_output_at = write_activity(agent, &activity, !captured.text.is_empty());
429
421
  write_agent_health(store, &team, agent_id, agent, &activity, last_output_at.as_deref())?;
422
+ let pane_info = matching_capture_pane_info(agent, &session, &window, &pane_infos);
423
+ let pane_id = pane_info
424
+ .as_ref()
425
+ .map(|info| info.pane_id.clone())
426
+ .or_else(|| agent_pane_id(agent));
427
+ let rollout_path = agent_rollout_path(agent).map(crate::provider::RolloutPath::new);
428
+ captures.insert(
429
+ AgentId::new(agent_id.clone()),
430
+ CapturedRuntimeFact {
431
+ team_key: team_key.clone(),
432
+ agent_id: AgentId::new(agent_id.clone()),
433
+ provider: agent.get("provider").and_then(Value::as_str).and_then(parse_provider),
434
+ session_name: Some(session),
435
+ window: Some(window),
436
+ pane_id,
437
+ scrollback_tail: captured.text,
438
+ pane_info,
439
+ agent_state_snapshot: agent.clone(),
440
+ stored_session_id: agent
441
+ .get("session_id")
442
+ .and_then(Value::as_str)
443
+ .map(str::to_string),
444
+ last_output_at,
445
+ rollout_path,
446
+ process_liveness: explicit_process_liveness(agent),
447
+ },
448
+ );
430
449
  }
431
- Ok(())
450
+ Ok(captures)
451
+ }
452
+
453
+ fn capture_leader_receiver(
454
+ &self,
455
+ state: &Value,
456
+ ) -> Option<runtime_observation::LeaderCaptureFact> {
457
+ let receiver = state.get("leader_receiver")?.clone();
458
+ let pane_id = receiver
459
+ .get("pane_id")
460
+ .and_then(Value::as_str)
461
+ .filter(|pane_id| !pane_id.is_empty())
462
+ .map(crate::transport::PaneId::new)?;
463
+ let captured = self
464
+ .transport
465
+ .capture(
466
+ &crate::transport::Target::Pane(pane_id.clone()),
467
+ crate::transport::CaptureRange::Tail(40),
468
+ )
469
+ .ok()?;
470
+ Some(runtime_observation::LeaderCaptureFact {
471
+ team_key: Some(crate::model::ids::TeamKey::new(
472
+ crate::state::projection::team_state_key(state),
473
+ )),
474
+ leader_receiver: Some(receiver),
475
+ pane_id: Some(pane_id),
476
+ scrollback_tail: captured.text,
477
+ })
432
478
  }
433
479
 
434
480
  /// #236 `worker.abnormal_exit` watcher.
@@ -692,13 +738,14 @@ impl Coordinator {
692
738
  let snapshot = state.clone();
693
739
  let team = crate::state::projection::team_state_key(&snapshot);
694
740
  let session_name = snapshot.get("session_name").and_then(Value::as_str).map(str::to_string);
695
- let auto_answer_allowed = runtime_approval_auto_answer_allowed();
696
741
  let mut dedup_updates = Vec::new();
697
742
  {
698
743
  let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
699
744
  return Ok(());
700
745
  };
701
746
  for (agent_id, agent) in agents {
747
+ let approval_policy = runtime_approval_policy_from_agent(agent);
748
+ let auto_answer_allowed = approval_policy.auto_answer_allowed();
702
749
  let Some(target) = runtime_approval_target(agent, session_name.as_deref()) else {
703
750
  clear_awaiting_human_confirm(agent);
704
751
  dedup_updates.push(AwaitingDedupUpdate::Clear {
@@ -753,13 +800,17 @@ impl Coordinator {
753
800
  let cleared = after
754
801
  .as_ref()
755
802
  .is_none_or(|after| after.prompt != prompt.prompt || after.tool != prompt.tool);
756
- event_log.write(
803
+ event_log.write(
757
804
  "runtime_approval.auto_approved",
758
805
  serde_json::json!({
759
806
  "agent_id": agent_id,
760
807
  "tool": prompt.tool,
761
808
  "choice": choice,
762
809
  "cleared": cleared,
810
+ "policy_source": approval_policy.source,
811
+ "inherited": approval_policy.inherited,
812
+ "explicit_yes_confirmed": approval_policy.explicit_yes_confirmed,
813
+ "worker_capability_above_leader": approval_policy.worker_capability_above_leader,
763
814
  }),
764
815
  )?;
765
816
  }
@@ -820,6 +871,18 @@ impl Coordinator {
820
871
  }),
821
872
  )?;
822
873
  }
874
+ "command_approval_requires_human" => {
875
+ event_log.write(
876
+ "runtime_approval.command_approval_requires_human",
877
+ serde_json::json!({
878
+ "agent_id": agent_id,
879
+ "tool": prompt.tool,
880
+ "command": prompt.command,
881
+ "kind": prompt.kind,
882
+ "prompt": prompt.prompt,
883
+ }),
884
+ )?;
885
+ }
823
886
  _ => {}
824
887
  }
825
888
  }
@@ -963,9 +1026,9 @@ fn base_tick_report(
963
1026
  stuck: collections.stuck,
964
1027
  idle_alerts: collections.idle_alerts,
965
1028
  deadlock_alerts: collections.deadlock_alerts,
966
- compaction: Vec::new(),
967
- session_drift: Vec::new(),
968
- api_errors: Vec::new(),
1029
+ compaction: collections.compaction,
1030
+ session_drift: collections.session_drift,
1031
+ api_errors: collections.api_errors,
969
1032
  results: collections.results,
970
1033
  }
971
1034
  }
@@ -977,6 +1040,9 @@ struct TickCollections {
977
1040
  stuck: Vec<AgentId>,
978
1041
  idle_alerts: Vec<IdleAlert>,
979
1042
  deadlock_alerts: Vec<DeadlockAlert>,
1043
+ compaction: Vec<CompactionResult>,
1044
+ session_drift: Vec<SessionDriftResult>,
1045
+ api_errors: Vec<LeaderApiError>,
980
1046
  results: Vec<CollectedResult>,
981
1047
  }
982
1048
 
@@ -1750,6 +1816,45 @@ fn capture_window_target(
1750
1816
  ))
1751
1817
  }
1752
1818
 
1819
+ fn matching_capture_pane_info(
1820
+ agent: &Value,
1821
+ session: &crate::transport::SessionName,
1822
+ window: &crate::transport::WindowName,
1823
+ pane_infos: &[crate::transport::PaneInfo],
1824
+ ) -> Option<crate::transport::PaneInfo> {
1825
+ if let Some(pane_id) = agent_pane_id(agent) {
1826
+ if let Some(info) = pane_infos.iter().find(|info| info.pane_id == pane_id) {
1827
+ return Some(info.clone());
1828
+ }
1829
+ }
1830
+ pane_infos
1831
+ .iter()
1832
+ .find(|info| {
1833
+ &info.session == session
1834
+ && info
1835
+ .window_name
1836
+ .as_ref()
1837
+ .is_some_and(|known_window| known_window == window)
1838
+ })
1839
+ .cloned()
1840
+ }
1841
+
1842
+ fn agent_pane_id(agent: &Value) -> Option<crate::transport::PaneId> {
1843
+ agent
1844
+ .get("pane_id")
1845
+ .and_then(Value::as_str)
1846
+ .filter(|pane_id| !pane_id.is_empty())
1847
+ .map(crate::transport::PaneId::new)
1848
+ }
1849
+
1850
+ fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
1851
+ ["rollout_path", "transcript_path", "session_log_path"]
1852
+ .into_iter()
1853
+ .find_map(|key| agent.get(key).and_then(Value::as_str))
1854
+ .filter(|path| !path.is_empty())
1855
+ .map(PathBuf::from)
1856
+ }
1857
+
1753
1858
  fn runtime_approval_target(agent: &Value, session_name: Option<&str>) -> Option<crate::transport::Target> {
1754
1859
  if let Some(pane_id) = agent
1755
1860
  .get("pane_id")
@@ -1780,10 +1885,58 @@ fn runtime_approval_key(raw: String) -> Option<crate::transport::Key> {
1780
1885
  }
1781
1886
  }
1782
1887
 
1783
- fn runtime_approval_auto_answer_allowed() -> bool {
1784
- crate::lifecycle::launch::detect_dangerous_approval()
1785
- .map(|safety| safety.enabled && !safety.worker_capability_above_leader)
1786
- .unwrap_or(false)
1888
+ #[derive(Debug, Clone)]
1889
+ struct RuntimeApprovalPolicy {
1890
+ enabled: bool,
1891
+ source: String,
1892
+ inherited: bool,
1893
+ explicit_yes_confirmed: bool,
1894
+ worker_capability_above_leader: bool,
1895
+ }
1896
+
1897
+ impl RuntimeApprovalPolicy {
1898
+ fn auto_answer_allowed(&self) -> bool {
1899
+ if !self.enabled {
1900
+ return false;
1901
+ }
1902
+ let source_allows = match self.source.as_str() {
1903
+ "leader_process" => self.inherited,
1904
+ "runtime_config" => self.explicit_yes_confirmed,
1905
+ _ => false,
1906
+ };
1907
+ source_allows
1908
+ && (!self.worker_capability_above_leader
1909
+ || (self.source == "runtime_config" && self.explicit_yes_confirmed))
1910
+ }
1911
+ }
1912
+
1913
+ fn runtime_approval_policy_from_agent(agent: &Value) -> RuntimeApprovalPolicy {
1914
+ let policy = agent
1915
+ .get("effective_approval_policy")
1916
+ .and_then(Value::as_object);
1917
+ RuntimeApprovalPolicy {
1918
+ enabled: policy
1919
+ .and_then(|p| p.get("enabled"))
1920
+ .and_then(Value::as_bool)
1921
+ .unwrap_or(false),
1922
+ source: policy
1923
+ .and_then(|p| p.get("source"))
1924
+ .and_then(Value::as_str)
1925
+ .unwrap_or("disabled")
1926
+ .to_string(),
1927
+ inherited: policy
1928
+ .and_then(|p| p.get("inherited"))
1929
+ .and_then(Value::as_bool)
1930
+ .unwrap_or(false),
1931
+ explicit_yes_confirmed: policy
1932
+ .and_then(|p| p.get("explicit_yes_confirmed"))
1933
+ .and_then(Value::as_bool)
1934
+ .unwrap_or(false),
1935
+ worker_capability_above_leader: policy
1936
+ .and_then(|p| p.get("worker_capability_above_leader"))
1937
+ .and_then(Value::as_bool)
1938
+ .unwrap_or(false),
1939
+ }
1787
1940
  }
1788
1941
 
1789
1942
  fn awaiting_human_confirm_payload(
@@ -152,6 +152,10 @@ pub enum OrphanReason {
152
152
  WorkspaceAlive,
153
153
  /// 无法解析 cmdline → 无法判定 workspace。
154
154
  CmdlineUnparsed,
155
+ /// workspace 仍存在,但 coordinator metadata 不指向该 pid/schema。
156
+ MetadataMismatch,
157
+ /// ps 列表仍有命令残留,但 pid 已不再存活。
158
+ PidNotRunning,
155
159
  }
156
160
 
157
161
  // ===========================================================================
@@ -553,20 +557,28 @@ pub struct DeadlockAlert {
553
557
  #[derive(Debug, Clone, PartialEq, Eq)]
554
558
  pub struct CompactionResult {
555
559
  pub agent_id: AgentId,
556
- pub raw: Value,
560
+ pub provider: Option<Provider>,
561
+ pub observed: bool,
562
+ pub reason: Option<String>,
563
+ pub recommendation: Option<String>,
557
564
  }
558
565
 
559
566
  /// `detect_session_drift` 结果(`messaging/session_drift.py`,step 11 拥有)。**PLACEHOLDER**。
560
567
  #[derive(Debug, Clone, PartialEq, Eq)]
561
568
  pub struct SessionDriftResult {
562
569
  pub agent_id: AgentId,
563
- pub raw: Value,
570
+ pub stored_session_id: Option<String>,
571
+ pub observed_session_id: Option<String>,
572
+ pub status: String,
564
573
  }
565
574
 
566
575
  /// `detect_leader_api_errors` 结果(`messaging/leader_api_errors.py`,step 11 拥有)。**PLACEHOLDER**。
567
576
  #[derive(Debug, Clone, PartialEq, Eq)]
568
577
  pub struct LeaderApiError {
569
- pub raw: Value,
578
+ pub provider: Option<Provider>,
579
+ pub pane_id: Option<String>,
580
+ pub fingerprint: String,
581
+ pub message: String,
570
582
  }
571
583
 
572
584
  /// `_collect_results_and_notify_watchers` 结果(`results.py:430`,step 11 拥有)。**PLACEHOLDER**。
@@ -5,6 +5,8 @@
5
5
  //! slice 2(待做):列**顺序**漂移的整表 rebuild(`ensure_table_layout`/`_rebuild_tables`)
6
6
  //! 与 `schema_diagnosis`,验 legacy_team_db_fixture + schema_migration 契约。
7
7
 
8
+ use std::time::Duration;
9
+
8
10
  use rusqlite::Connection;
9
11
 
10
12
  use crate::db::DbError;
@@ -66,11 +68,44 @@ const LEADER_NOTIFICATION_LOG_COLUMNS: &[&str] = &[
66
68
 
67
69
  /// 打开 `team.db` 并设 pragmas(`core.py:60-61`:busy_timeout=30000 + WAL)。
68
70
  pub fn open_db(path: &std::path::Path) -> Result<Connection, DbError> {
69
- let conn = Connection::open(path)?;
70
- conn.execute_batch("PRAGMA busy_timeout=30000; PRAGMA journal_mode=WAL;")?;
71
+ let existed = path.exists();
72
+ let conn = retry_sqlite(|| Connection::open(path))?;
73
+ conn.busy_timeout(Duration::from_millis(30_000))?;
74
+ if !existed {
75
+ retry_sqlite(|| conn.execute_batch("PRAGMA journal_mode=WAL;"))?;
76
+ }
71
77
  Ok(conn)
72
78
  }
73
79
 
80
+ fn retry_sqlite<T>(mut op: impl FnMut() -> rusqlite::Result<T>) -> Result<T, DbError> {
81
+ let mut last_error = None;
82
+ for attempt in 0..8 {
83
+ match op() {
84
+ Ok(value) => return Ok(value),
85
+ Err(error) if sqlite_busy_or_locked(&error) => {
86
+ last_error = Some(error);
87
+ std::thread::sleep(Duration::from_millis(25 * (attempt + 1)));
88
+ }
89
+ Err(error) => return Err(error.into()),
90
+ }
91
+ }
92
+ match last_error {
93
+ Some(error) => Err(error.into()),
94
+ None => Err(DbError::Schema("sqlite retry exhausted without an error".to_string())),
95
+ }
96
+ }
97
+
98
+ fn sqlite_busy_or_locked(error: &rusqlite::Error) -> bool {
99
+ matches!(
100
+ error,
101
+ rusqlite::Error::SqliteFailure(err, _)
102
+ if matches!(
103
+ err.code,
104
+ rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
105
+ )
106
+ )
107
+ }
108
+
74
109
  /// `pragma table_info(table)` 的列名序(`schema_migration.py:table_layout`)。
75
110
  pub fn table_layout(conn: &Connection, table: &str) -> Result<Vec<String>, DbError> {
76
111
  // table 来自固定常量名,非用户输入 → format 安全。