@team-agent/installer 0.5.50 → 0.5.52

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 (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -18,13 +18,15 @@ pub fn start_agent(
18
18
  team: Option<&str>,
19
19
  ) -> Result<StartAgentOutcome, LifecycleError> {
20
20
  let paths = lifecycle_paths(workspace, team)?;
21
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
22
+ let team = canonical_team.as_deref();
21
23
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
22
24
  workspace: &paths.run_workspace,
23
25
  operation: "start-agent",
24
26
  team,
25
27
  agent_id: Some(agent_id),
26
28
  })?;
27
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
29
+ let transport = paths.tmux_backend()?;
28
30
  start_agent_at_paths(
29
31
  &paths.run_workspace,
30
32
  &paths.spec_workspace,
@@ -54,9 +56,12 @@ pub fn start_agent_with_transport(
54
56
  Err(_) if team.is_none() => LifecyclePaths {
55
57
  run_workspace: workspace.to_path_buf(),
56
58
  spec_workspace: workspace.to_path_buf(),
59
+ selected: None,
57
60
  },
58
61
  Err(error) => return Err(error),
59
62
  };
63
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
64
+ let team = canonical_team.as_deref();
60
65
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
61
66
  workspace: &paths.run_workspace,
62
67
  operation: "start-agent",
@@ -778,13 +783,15 @@ pub fn stop_agent(
778
783
  team: Option<&str>,
779
784
  ) -> Result<StopAgentReport, LifecycleError> {
780
785
  let paths = lifecycle_paths(workspace, team)?;
786
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
787
+ let team = canonical_team.as_deref();
781
788
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
782
789
  workspace: &paths.run_workspace,
783
790
  operation: "stop-agent",
784
791
  team,
785
792
  agent_id: Some(agent_id),
786
793
  })?;
787
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
794
+ let transport = paths.tmux_backend()?;
788
795
  stop_agent_at_paths(
789
796
  &paths.run_workspace,
790
797
  &paths.spec_workspace,
@@ -801,6 +808,8 @@ pub fn stop_agent_with_transport(
801
808
  transport: &dyn crate::transport::Transport,
802
809
  ) -> Result<StopAgentReport, LifecycleError> {
803
810
  let paths = lifecycle_paths(workspace, team)?;
811
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
812
+ let team = canonical_team.as_deref();
804
813
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
805
814
  workspace: &paths.run_workspace,
806
815
  operation: "stop-agent",
@@ -840,9 +849,12 @@ pub(crate) fn start_agent_at_paths_for_recovery(
840
849
  Err(_) if team.is_none() => LifecyclePaths {
841
850
  run_workspace: workspace.to_path_buf(),
842
851
  spec_workspace: workspace.to_path_buf(),
852
+ selected: None,
843
853
  },
844
854
  Err(error) => return Err(RecoveryError::Lifecycle(error.to_string())),
845
855
  };
856
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
857
+ let team = canonical_team.as_deref();
846
858
  // Best-effort stop: if the live pane still exists we tear it down so the
847
859
  // subsequent start creates a fresh provider process instead of a Noop.
848
860
  // Stop errors are absorbed into the start attempt result — the important
@@ -884,26 +896,38 @@ pub(super) fn stop_agent_at_paths(
884
896
  transport: &dyn crate::transport::Transport,
885
897
  ) -> Result<StopAgentReport, LifecycleError> {
886
898
  // golden operations.py:64-66: resolve_team_scoped_state -> owner gate, BEFORE the unknown-worker raise.
887
- let mut state = resolve_team_scoped_state_or_refuse(workspace, team)?;
888
- crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
889
- let spec = load_team_spec(spec_workspace)?;
890
- let agent = find_spec_agent(&spec, agent_id).ok_or_else(|| unknown_worker(agent_id))?;
891
- let session_name = state_session_name_from_spec(&state, &spec);
892
- let window = state
893
- .get("agents")
894
- .and_then(|v| v.get(agent_id.as_str()))
895
- .and_then(|v| v.get("window"))
896
- .and_then(|v| v.as_str())
897
- .filter(|s| !s.is_empty())
898
- .unwrap_or_else(|| agent_id.as_str())
899
- .to_string();
900
- let pane_id = state
901
- .get("agents")
902
- .and_then(|v| v.get(agent_id.as_str()))
903
- .and_then(|v| v.get("pane_id"))
904
- .and_then(|v| v.as_str())
905
- .filter(|pane| !pane.is_empty())
906
- .map(crate::transport::PaneId::new);
899
+ let seat = super::remove::resolve_seat(workspace, spec_workspace, agent_id, team, transport)?;
900
+ if seat.consistency == super::remove::SeatConsistency::Absent {
901
+ return Err(unknown_worker(agent_id));
902
+ }
903
+ let mut state = seat.state;
904
+ let spec = seat.spec;
905
+ let state_agent = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
906
+ // Persisted-only seats must remain stoppable. Build the minimal provider
907
+ // projection consumed by mark_agent_stopped instead of requiring a spec
908
+ // row that is precisely what recovery is repairing.
909
+ let fallback_agent = YamlValue::Map(vec![(
910
+ "provider".to_string(),
911
+ YamlValue::Str(
912
+ state_agent
913
+ .and_then(|agent| agent.get("provider"))
914
+ .and_then(|value| value.as_str())
915
+ .unwrap_or("codex")
916
+ .to_string(),
917
+ ),
918
+ )]);
919
+ let agent = find_spec_agent(&spec, agent_id).unwrap_or(&fallback_agent);
920
+ let session_name = seat.session;
921
+ let window = seat.window;
922
+ let pane_id = seat.physical.map(|pane| pane.pane_id).or_else(|| {
923
+ state
924
+ .get("agents")
925
+ .and_then(|agents| agents.get(agent_id.as_str()))
926
+ .and_then(|agent| agent.get("pane_id"))
927
+ .and_then(serde_json::Value::as_str)
928
+ .filter(|pane| !pane.is_empty())
929
+ .map(crate::transport::PaneId::new)
930
+ });
907
931
  let target_str = pane_id
908
932
  .as_ref()
909
933
  .map(|pane| pane.as_str().to_string())
@@ -995,12 +1019,15 @@ pub(super) fn stop_agent_at_paths(
995
1019
  }
996
1020
  close_agent_display(&mut state, agent_id);
997
1021
  mark_agent_stopped(&mut state, agent_id, agent, &window)?;
1022
+ let team_key = restart_projection_team_key(&state, team);
998
1023
  // golden operations.py:95: save_team_scoped_state (team projection) — NOT a raw save, so a
999
1024
  // multi-team workspace keeps the other teams' persisted runtime state instead of being clobbered.
1000
- crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority(
1001
- workspace,
1025
+ crate::state::repository::StateRepository::new(workspace).save(
1026
+ crate::state::repository::StateWriteIntent::StopAgent {
1027
+ team_key: &team_key,
1028
+ agent_id: agent_id.as_str(),
1029
+ },
1002
1030
  &state,
1003
- &[agent_id.as_str()],
1004
1031
  )
1005
1032
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1006
1033
  // golden operations.py:96-99: snapshot (side-effect), then state_file = write_team_state path.
@@ -1202,13 +1229,15 @@ pub fn reset_agent(
1202
1229
  });
1203
1230
  }
1204
1231
  let paths = lifecycle_paths(workspace, team)?;
1232
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
1233
+ let team = canonical_team.as_deref();
1205
1234
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
1206
1235
  workspace: &paths.run_workspace,
1207
1236
  operation: "reset-agent",
1208
1237
  team,
1209
1238
  agent_id: Some(agent_id),
1210
1239
  })?;
1211
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
1240
+ let transport = paths.tmux_backend()?;
1212
1241
  reset_agent_at_paths(
1213
1242
  &paths.run_workspace,
1214
1243
  &paths.spec_workspace,
@@ -1234,6 +1263,8 @@ pub fn reset_agent_with_transport(
1234
1263
  });
1235
1264
  }
1236
1265
  let paths = lifecycle_paths(workspace, team)?;
1266
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
1267
+ let team = canonical_team.as_deref();
1237
1268
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
1238
1269
  workspace: &paths.run_workspace,
1239
1270
  operation: "reset-agent",
@@ -1447,10 +1478,13 @@ fn reset_agent_at_paths(
1447
1478
  sync_restart_team_projections(&mut state, &team_key);
1448
1479
  // golden operations.py (reset): save_team_scoped_state on the team projection — same multi-team
1449
1480
  // preservation as stop, not a raw save_runtime_state.
1450
- crate::state::projection::save_team_scoped_state_with_tombstone_lifecycle_topology_authority(
1451
- workspace,
1481
+ crate::state::repository::StateRepository::new(workspace).save(
1482
+ crate::state::repository::StateWriteIntent::ResetAgent {
1483
+ team_key: &team_key,
1484
+ agent_id: agent_id.as_str(),
1485
+ discard_session,
1486
+ },
1452
1487
  &state,
1453
- &[agent_id.as_str()],
1454
1488
  )
1455
1489
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1456
1490
  // golden operations.py:125: write_team_state after the discard-save (the intermediate stopped snapshot).
@@ -373,8 +373,8 @@ pub(super) fn spawn_agent_window(
373
373
  let tmux_endpoint = transport.tmux_endpoint();
374
374
  let event_log = crate::event_log::EventLog::new(workspace);
375
375
  let _ = event_log.write(
376
- "provider.worker.spawn_argv",
377
- serde_json::json!({
376
+ crate::event_log::PROVIDER_WORKER_SPAWN_ARGV,
377
+ crate::event_log::provider_worker_spawn_argv_fields(serde_json::json!({
378
378
  "agent_id": agent_id.as_str(),
379
379
  "provider": provider,
380
380
  "argv": plan.argv,
@@ -388,7 +388,7 @@ pub(super) fn spawn_agent_window(
388
388
  "source": "restart",
389
389
  "tmux_endpoint": tmux_endpoint,
390
390
  "tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
391
- }),
391
+ })),
392
392
  );
393
393
  }
394
394
 
@@ -817,11 +817,13 @@ pub(super) fn save_restart_projected_state_with_capture_backfill_skip(
817
817
  topology_authority_agent_ids: &[&str],
818
818
  ) -> Result<(), LifecycleError> {
819
819
  sync_restart_team_projections(state, team_key);
820
- crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
821
- workspace,
820
+ crate::state::repository::StateRepository::new(workspace).save(
821
+ crate::state::repository::StateWriteIntent::RestartTeam {
822
+ team_key,
823
+ topology_authority_agent_ids,
824
+ skip_capture_backfill_agent_ids,
825
+ },
822
826
  state,
823
- skip_capture_backfill_agent_ids,
824
- topology_authority_agent_ids,
825
827
  )
826
828
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
827
829
  }
@@ -1316,21 +1318,16 @@ pub(crate) fn converge_missing_provider_sessions(
1316
1318
  poll_interval,
1317
1319
  restart_required_missing_session_agent_ids,
1318
1320
  |progress| {
1319
- let pending_agent_ids = progress.pending_agent_ids.clone();
1320
1321
  write_session_convergence_progress_event(
1321
1322
  workspace,
1322
1323
  serde_json::json!({
1323
- "ts": chrono::Utc::now().to_rfc3339(),
1324
- "event": "provider.session.converging",
1325
1324
  "iteration": progress.iteration,
1326
1325
  "elapsed_ms": progress.elapsed_ms,
1327
1326
  "deadline_ms": progress.deadline_ms,
1328
1327
  "changed": progress.changed,
1329
1328
  "assigned": progress.assigned,
1330
1329
  "missing": progress.missing,
1331
- "required_missing": progress.required_missing_agent_ids.clone(),
1332
1330
  "required_missing_agent_ids": progress.required_missing_agent_ids,
1333
- "pending": pending_agent_ids,
1334
1331
  "pending_agent_ids": progress.pending_agent_ids,
1335
1332
  "candidate_count_by_agent": progress.candidate_count_by_agent,
1336
1333
  "remaining_ms": progress.remaining_ms,
@@ -1344,22 +1341,11 @@ pub(crate) fn converge_missing_provider_sessions(
1344
1341
 
1345
1342
  fn write_session_convergence_progress_event(
1346
1343
  workspace: &Path,
1347
- event: serde_json::Value,
1344
+ fields: serde_json::Value,
1348
1345
  ) -> Result<(), String> {
1349
- use std::io::Write as _;
1350
-
1351
- let path = workspace.join(".team").join("logs").join("events.jsonl");
1352
- if let Some(parent) = path.parent() {
1353
- std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
1354
- }
1355
- let line = serde_json::to_string(&event).map_err(|e| e.to_string())?;
1356
- let mut file = std::fs::OpenOptions::new()
1357
- .create(true)
1358
- .append(true)
1359
- .open(path)
1360
- .map_err(|e| e.to_string())?;
1361
- file.write_all(line.as_bytes())
1362
- .and_then(|_| file.write_all(b"\n"))
1346
+ crate::event_log::EventLog::new(workspace)
1347
+ .write(crate::event_log::PROVIDER_SESSION_CONVERGING, fields)
1348
+ .map(|_| ())
1363
1349
  .map_err(|e| e.to_string())
1364
1350
  }
1365
1351
 
@@ -1837,6 +1823,46 @@ mod e36_transcript_backing_tests {
1837
1823
  }
1838
1824
  }
1839
1825
 
1826
+ #[test]
1827
+ fn convergence_event_uses_central_scrub_and_preserves_required_failure() {
1828
+ let scratch = ScratchDir::new("event-log-central");
1829
+ let marker = "synthetic-convergence-marker";
1830
+ write_session_convergence_progress_event(
1831
+ scratch.path(),
1832
+ serde_json::json!({
1833
+ "diagnostic": format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\""),
1834
+ "required_missing_agent_ids": ["worker"],
1835
+ "pending_agent_ids": ["worker"],
1836
+ }),
1837
+ )
1838
+ .expect("central EventLog write");
1839
+ let bytes = std::fs::read_to_string(
1840
+ scratch
1841
+ .path()
1842
+ .join(".team")
1843
+ .join("logs")
1844
+ .join("events.jsonl"),
1845
+ )
1846
+ .expect("events");
1847
+ assert!(!bytes.contains(marker));
1848
+ assert!(bytes.contains("[REDACTED]"));
1849
+
1850
+ let blocked = ScratchDir::new("event-log-required");
1851
+ std::fs::create_dir_all(
1852
+ blocked
1853
+ .path()
1854
+ .join(".team")
1855
+ .join("logs")
1856
+ .join("events.jsonl"),
1857
+ )
1858
+ .expect("occupy event path with directory");
1859
+ assert!(write_session_convergence_progress_event(
1860
+ blocked.path(),
1861
+ serde_json::json!({"pending_agent_ids": ["worker"]}),
1862
+ )
1863
+ .is_err());
1864
+ }
1865
+
1840
1866
  // E36 fix-B RED→GREEN: a real Claude worker that sent a message has its session
1841
1867
  // transcript landed at <projects_root>/<slug>/<session_id>.jsonl, but neither
1842
1868
  // rollout_path was persisted to state nor a session.captured event was logged.
@@ -1841,11 +1841,13 @@ fn save_restart_state_with_lifecycle_topology_authority_and_capture_backfill_ski
1841
1841
  .map(String::as_str)
1842
1842
  .collect::<Vec<_>>();
1843
1843
  sync_restart_team_projections(state, team_key);
1844
- crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
1845
- workspace,
1844
+ crate::state::repository::StateRepository::new(workspace).save(
1845
+ crate::state::repository::StateWriteIntent::RestartTeam {
1846
+ team_key,
1847
+ topology_authority_agent_ids: &topology_agent_ids,
1848
+ skip_capture_backfill_agent_ids: &skip_capture_backfill_agent_ids,
1849
+ },
1846
1850
  state,
1847
- &skip_capture_backfill_agent_ids,
1848
- &topology_agent_ids,
1849
1851
  )
1850
1852
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
1851
1853
  }
@@ -1858,7 +1860,11 @@ fn save_restart_session_repairs(
1858
1860
  ) -> Result<(), LifecycleError> {
1859
1861
  let repairs = collect_session_repair_fields(state, agent_ids);
1860
1862
  sync_restart_team_projections(state, team_key);
1861
- match crate::state::projection::save_team_scoped_state(workspace, state) {
1863
+ let repository = crate::state::repository::StateRepository::new(workspace);
1864
+ match repository.save(
1865
+ crate::state::repository::StateWriteIntent::RestartSessionRepair { team_key },
1866
+ state,
1867
+ ) {
1862
1868
  Ok(()) => Ok(()),
1863
1869
  Err(crate::state::StateError::SaveConflict(_)) => {
1864
1870
  let mut latest =
@@ -1868,7 +1874,11 @@ fn save_restart_session_repairs(
1868
1874
  apply_session_repair_fields(&mut latest, agent_id, repair);
1869
1875
  }
1870
1876
  sync_restart_team_projections(&mut latest, team_key);
1871
- crate::state::projection::save_team_scoped_state(workspace, &latest)
1877
+ repository
1878
+ .save(
1879
+ crate::state::repository::StateWriteIntent::RestartSessionRepair { team_key },
1880
+ &latest,
1881
+ )
1872
1882
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1873
1883
  *state = latest;
1874
1884
  Ok(())
@@ -2266,8 +2276,8 @@ fn write_fake_harness_spawn_argv_event(
2266
2276
  tmux_endpoint_source: Option<&str>,
2267
2277
  ) {
2268
2278
  let _ = crate::event_log::EventLog::new(workspace).write(
2269
- "provider.worker.spawn_argv",
2270
- serde_json::json!({
2279
+ crate::event_log::PROVIDER_WORKER_SPAWN_ARGV,
2280
+ crate::event_log::provider_worker_spawn_argv_fields(serde_json::json!({
2271
2281
  "agent_id": decision.agent_id.as_str(),
2272
2282
  "provider": agent_provider(agent),
2273
2283
  "argv": [],
@@ -2277,7 +2287,7 @@ fn write_fake_harness_spawn_argv_event(
2277
2287
  "source": "restart",
2278
2288
  "tmux_endpoint": transport.tmux_endpoint(),
2279
2289
  "tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
2280
- }),
2290
+ })),
2281
2291
  );
2282
2292
  }
2283
2293
 
@@ -2924,15 +2934,7 @@ fn write_restart_resume_decision_event(
2924
2934
  forced_fresh_convergence: Option<&crate::session_capture::SessionConvergence>,
2925
2935
  unresumable: Option<&crate::lifecycle::types::UnresumableWorker>,
2926
2936
  ) -> Result<(), LifecycleError> {
2927
- use std::io::Write as _;
2928
-
2929
- let path = workspace.join(".team").join("logs").join("events.jsonl");
2930
- if let Some(parent) = path.parent() {
2931
- std::fs::create_dir_all(parent).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
2932
- }
2933
2937
  let mut event = serde_json::json!({
2934
- "ts": chrono::Utc::now().to_rfc3339(),
2935
- "event": crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
2936
2938
  "worker_id": worker_id,
2937
2939
  "has_first_send_at": first_send_at.is_some(),
2938
2940
  "has_session_id": session_id.is_some(),
@@ -3039,15 +3041,12 @@ fn write_restart_resume_decision_event(
3039
3041
  }
3040
3042
  }
3041
3043
  }
3042
- let line =
3043
- serde_json::to_string(&event).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
3044
- let mut file = std::fs::OpenOptions::new()
3045
- .create(true)
3046
- .append(true)
3047
- .open(&path)
3048
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
3049
- file.write_all(line.as_bytes())
3050
- .and_then(|_| file.write_all(b"\n"))
3044
+ crate::event_log::EventLog::new(workspace)
3045
+ .write(
3046
+ crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
3047
+ event,
3048
+ )
3049
+ .map(|_| ())
3051
3050
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
3052
3051
  }
3053
3052
 
@@ -3120,10 +3119,16 @@ fn restart_candidate_from_state(
3120
3119
  .filter(|s| !s.is_empty())
3121
3120
  .map(SessionName::new)
3122
3121
  .unwrap_or_else(|| SessionName::new(team_name));
3122
+ let retired = retired_agent_ids(state);
3123
3123
  let mut agents = state
3124
3124
  .get("agents")
3125
3125
  .and_then(serde_json::Value::as_object)
3126
- .map(|map| map.keys().map(AgentId::new).collect::<Vec<_>>())
3126
+ .map(|map| {
3127
+ map.keys()
3128
+ .filter(|agent_id| !retired.contains(agent_id.as_str()))
3129
+ .map(AgentId::new)
3130
+ .collect::<Vec<_>>()
3131
+ })
3127
3132
  .unwrap_or_default();
3128
3133
  agents.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3129
3134
  RestartCandidate {
@@ -3136,6 +3141,19 @@ fn restart_candidate_from_state(
3136
3141
  }
3137
3142
  }
3138
3143
 
3144
+ fn retired_agent_ids(state: &serde_json::Value) -> std::collections::BTreeSet<String> {
3145
+ state
3146
+ .get("agent_lifecycle")
3147
+ .and_then(serde_json::Value::as_object)
3148
+ .into_iter()
3149
+ .flat_map(|lifecycle| lifecycle.iter())
3150
+ .filter(|(_, entry)| {
3151
+ entry.get("state").and_then(serde_json::Value::as_str) == Some("retired")
3152
+ })
3153
+ .map(|(agent_id, _)| agent_id.clone())
3154
+ .collect()
3155
+ }
3156
+
3139
3157
  /// E5 task#3 / RC-A6a:每次 restart 都以**角色定义**(team_dir 的 TEAM.md+agents/*.md)
3140
3158
  /// compile_team 重建 runtime spec(覆盖),保留运行期 override(session_name 必须延续,
3141
3159
  /// 否则 tmux session 对不上)。写到 .team/runtime/<team_key>/team.spec.yaml。
@@ -3205,6 +3223,7 @@ fn rebuild_runtime_spec_from_roles(
3205
3223
  // 静态 team_dir/agents/*.md + state 记录的 dynamic role source。缺文件 fail-closed
3206
3224
  // 三行式,不静默 prune 已 live 的 helper(persist SaveConflict 保护继续生效)。
3207
3225
  merge_state_dynamic_role_files(&mut spec, run_workspace, &team_dir, team_key, state)?;
3226
+ filter_retired_agents_from_spec(&mut spec, &retired_agent_ids(state));
3208
3227
  // 写 runtime spec(覆盖,原子 tmp+rename;Bug2)。
3209
3228
  let spec_path = crate::model::paths::runtime_spec_path(run_workspace, team_key);
3210
3229
  crate::lifecycle::launch::write_spec_atomic(&spec_path, &spec)?;
@@ -3217,6 +3236,15 @@ fn rebuild_runtime_spec_from_roles(
3217
3236
  Ok(spec)
3218
3237
  }
3219
3238
 
3239
+ fn filter_retired_agents_from_spec(
3240
+ spec: &mut YamlValue,
3241
+ retired: &std::collections::BTreeSet<String>,
3242
+ ) {
3243
+ for agent_id in retired {
3244
+ *spec = super::remove::spec_without_agent(spec, &AgentId::new(agent_id));
3245
+ }
3246
+ }
3247
+
3220
3248
  /// 0.5.30 (`add-agent-restart-saveconflict-locate.md` §4/§11): 把 add-agent
3221
3249
  /// 写入 `state.agents.<id>.dynamic_role_file` 的动态 role 文档合并回 restart
3222
3250
  /// 重建 spec。规则:
@@ -3419,6 +3447,118 @@ mod tests {
3419
3447
  TransportError,
3420
3448
  };
3421
3449
 
3450
+ #[test]
3451
+ fn retired_filter_clears_every_spec_reference_via_remove_primitive() {
3452
+ let mut spec = crate::model::yaml::loads(
3453
+ r#"
3454
+ name: retirement-filter
3455
+ agents:
3456
+ - id: gone
3457
+ - id: keep
3458
+ runtime:
3459
+ startup_order:
3460
+ - gone
3461
+ - keep
3462
+ routing:
3463
+ default_assignee: gone
3464
+ rules:
3465
+ - match: gone-task
3466
+ assign_to: gone
3467
+ - match: keep-task
3468
+ assign_to: keep
3469
+ tasks:
3470
+ - title: gone-task
3471
+ assignee: gone
3472
+ - title: keep-task
3473
+ assignee: keep
3474
+ "#,
3475
+ )
3476
+ .expect("fixture spec");
3477
+ let retired = std::collections::BTreeSet::from(["gone".to_string()]);
3478
+
3479
+ filter_retired_agents_from_spec(&mut spec, &retired);
3480
+
3481
+ let agents = spec
3482
+ .get("agents")
3483
+ .and_then(YamlValue::as_list)
3484
+ .expect("agents");
3485
+ assert!(agents.iter().all(|agent| {
3486
+ agent.get("id").and_then(YamlValue::as_str) != Some("gone")
3487
+ }));
3488
+ let startup_order = spec
3489
+ .get("runtime")
3490
+ .and_then(|runtime| runtime.get("startup_order"))
3491
+ .and_then(YamlValue::as_list)
3492
+ .expect("startup_order");
3493
+ assert!(startup_order
3494
+ .iter()
3495
+ .all(|agent_id| agent_id.as_str() != Some("gone")));
3496
+ let routing = spec.get("routing").expect("routing");
3497
+ assert_ne!(
3498
+ routing
3499
+ .get("default_assignee")
3500
+ .and_then(YamlValue::as_str),
3501
+ Some("gone"),
3502
+ "spec_default_assignee must not return a retired agent"
3503
+ );
3504
+ let rules = routing
3505
+ .get("rules")
3506
+ .and_then(YamlValue::as_list)
3507
+ .expect("routing rules");
3508
+ assert!(rules.iter().all(|rule| {
3509
+ rule.get("assign_to").and_then(YamlValue::as_str) != Some("gone")
3510
+ }));
3511
+ let tasks = spec
3512
+ .get("tasks")
3513
+ .and_then(YamlValue::as_list)
3514
+ .expect("tasks");
3515
+ assert!(tasks.iter().all(|task| {
3516
+ task.get("assignee").and_then(YamlValue::as_str) != Some("gone")
3517
+ }));
3518
+ }
3519
+
3520
+ #[test]
3521
+ fn resume_decision_event_uses_central_scrub_and_preserves_required_failure() {
3522
+ let marker = "synthetic-resume-decision-marker";
3523
+ let workspace =
3524
+ std::env::temp_dir().join(format!("ta-resume-decision-event-{}", std::process::id()));
3525
+ let _ = std::fs::remove_dir_all(&workspace);
3526
+ write_restart_resume_decision_event(
3527
+ &workspace,
3528
+ "worker",
3529
+ Some(format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\"")),
3530
+ None,
3531
+ false,
3532
+ "fresh_start",
3533
+ false,
3534
+ None,
3535
+ None,
3536
+ )
3537
+ .expect("central EventLog write");
3538
+ let bytes =
3539
+ std::fs::read_to_string(workspace.join(".team").join("logs").join("events.jsonl"))
3540
+ .expect("events");
3541
+ assert!(!bytes.contains(marker));
3542
+ assert!(bytes.contains("[REDACTED]"));
3543
+ let _ = std::fs::remove_dir_all(&workspace);
3544
+
3545
+ std::fs::create_dir_all(workspace.join(".team").join("logs").join("events.jsonl"))
3546
+ .expect("occupy event path with directory");
3547
+ let result = write_restart_resume_decision_event(
3548
+ &workspace,
3549
+ "worker",
3550
+ None,
3551
+ None,
3552
+ false,
3553
+ "fresh_start",
3554
+ false,
3555
+ None,
3556
+ None,
3557
+ );
3558
+ assert!(matches!(result, Err(LifecycleError::StatePersist(_))));
3559
+ let _ = std::fs::remove_dir_all(workspace);
3560
+ }
3561
+
3422
3562
  struct RespawnEpochTransport;
3423
3563
 
3424
3564
  impl Transport for RespawnEpochTransport {