@team-agent/installer 0.5.65 → 0.5.68

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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -882,6 +882,16 @@ fn merge_agent_projection(
882
882
  if !tombstoned_for_projection
883
883
  && latest_has_protected_live_topology(latest_agent, team_alive)
884
884
  {
885
+ // A lifecycle-authoritative writer may have loaded its whole
886
+ // projection before this peer finalized. Preserve the complete
887
+ // latest peer row rather than treating that untouched omission
888
+ // as an intentional delete. Ordinary saves (with no topology
889
+ // authority) retain the conflict so omission cannot clobber a
890
+ // live row.
891
+ if !topology_update_agent_ids.is_empty() {
892
+ slot.insert(latest_agent.clone());
893
+ continue;
894
+ }
885
895
  return Err(save_conflict(
886
896
  projection,
887
897
  agent_id,
@@ -896,7 +906,20 @@ fn merge_agent_projection(
896
906
  if !tombstoned_for_projection && !topology_update_agent_ids.contains(agent_id) {
897
907
  let fields = topology_conflict_fields(existing.get(), latest_agent, team_alive);
898
908
  if !fields.is_empty() {
899
- return Err(save_conflict(projection, agent_id, fields));
909
+ if !topology_update_agent_ids.is_empty()
910
+ && lifecycle_peer_snapshot_is_stale(existing.get(), latest_agent)
911
+ {
912
+ // A lifecycle writer may have loaded a complete
913
+ // peer row before that peer finalized. Its own
914
+ // topology authority is limited to the IDs it
915
+ // explicitly reports; preserve the latest peer
916
+ // row instead of surfacing a stale self-conflict.
917
+ preserve_latest_topology(existing.get_mut(), latest_agent);
918
+ } else if lifecycle_placeholder(existing.get()) {
919
+ preserve_latest_topology(existing.get_mut(), latest_agent);
920
+ } else {
921
+ return Err(save_conflict(projection, agent_id, fields));
922
+ }
900
923
  }
901
924
  }
902
925
  if !skip_capture_backfill || !skip_capture_backfill_agent_ids.contains(agent_id) {
@@ -908,6 +931,58 @@ fn merge_agent_projection(
908
931
  Ok(())
909
932
  }
910
933
 
934
+ fn lifecycle_placeholder(agent: &Value) -> bool {
935
+ matches!(
936
+ agent.get("status").and_then(Value::as_str),
937
+ Some("reserved" | "starting")
938
+ )
939
+ }
940
+
941
+ fn lifecycle_peer_snapshot_is_stale(incoming: &Value, latest: &Value) -> bool {
942
+ // A concurrent clone/restart can have promoted its peer to `running`
943
+ // while its capture tuple is still pending. That row is still a
944
+ // lifecycle snapshot, not a completed topology observation; preserving
945
+ // the latest peer avoids making the concurrent writer lose on a pane
946
+ // binding it never owned. A completed row remains epoch-ordered below,
947
+ // so a newer/equal completed topology still conflicts.
948
+ if incoming
949
+ .get("_pending_session_id")
950
+ .and_then(Value::as_str)
951
+ .is_some_and(|session| !session.is_empty())
952
+ {
953
+ return true;
954
+ }
955
+ let Some(incoming_epoch) = incoming.get("spawn_epoch").and_then(Value::as_u64) else {
956
+ return false;
957
+ };
958
+ let Some(latest_epoch) = latest.get("spawn_epoch").and_then(Value::as_u64) else {
959
+ return false;
960
+ };
961
+ incoming_epoch < latest_epoch
962
+ }
963
+
964
+ fn preserve_latest_topology(incoming: &mut Value, latest: &Value) {
965
+ let Some(incoming) = incoming.as_object_mut() else {
966
+ return;
967
+ };
968
+ // A stale add/clone writer can carry a placeholder row (`starting`) after
969
+ // a peer has completed its spawn. Preserve the completed lifecycle and
970
+ // capture tuple together with topology; otherwise a later peer save can
971
+ // regress the finalized row back to `starting` while returning success.
972
+ if latest.get("status").and_then(Value::as_str) == Some("running") {
973
+ // The finalized row is authoritative as a whole. Copying a hand-picked
974
+ // field list can leave stale capture/metadata keys from the placeholder
975
+ // and is not equivalent to preserving the peer that already finalized.
976
+ *incoming = latest.as_object().cloned().unwrap_or_default();
977
+ return;
978
+ }
979
+ for field in LIVE_TOPOLOGY_FIELDS {
980
+ if let Some(value) = latest.get(field).filter(|value| json_truthy(value)) {
981
+ incoming.insert(field.to_string(), value.clone());
982
+ }
983
+ }
984
+ }
985
+
911
986
  fn save_conflict(projection: &str, agent_id: &str, fields: Vec<&'static str>) -> StateError {
912
987
  StateError::SaveConflict(format!(
913
988
  "agent_id={agent_id} projection={projection} conflicting_fields={}",
@@ -2003,13 +2078,8 @@ mod tests {
2003
2078
  "target": {"agent_id": "target", "provider": "codex", "status": "stopped"}
2004
2079
  },
2005
2080
  });
2006
- save_runtime_state_with_lifecycle_topology_authority(
2007
- &ws,
2008
- &incoming,
2009
- "team-a",
2010
- &["target"],
2011
- )
2012
- .unwrap();
2081
+ save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["target"])
2082
+ .unwrap();
2013
2083
  let target = read_state(&ws).pointer("/agents/target").cloned().unwrap();
2014
2084
  assert!(
2015
2085
  target.get("pane_id").is_none(),
@@ -2021,6 +2091,229 @@ mod tests {
2021
2091
  );
2022
2092
  }
2023
2093
 
2094
+ #[test]
2095
+ fn lifecycle_authority_preserves_stale_omitted_finalized_peer() {
2096
+ let latest_state = || {
2097
+ json!({
2098
+ "session_name": "team-a",
2099
+ "active_team_key": "team-a",
2100
+ "agents": {
2101
+ "f1": {
2102
+ "agent_id": "f1",
2103
+ "provider": "codex",
2104
+ "role": "Developer",
2105
+ "model": "gpt-5.5",
2106
+ "status": "running",
2107
+ "session_id": "session-f1",
2108
+ "rollout_path": "/tmp/f1.jsonl",
2109
+ "captured_at": "2026-08-28T00:00:00Z",
2110
+ "captured_via": "contract-fixture",
2111
+ "attribution_confidence": "high",
2112
+ "capture_state": "complete",
2113
+ "spawn_cwd": "/tmp/f1",
2114
+ "owner_team_id": "team-a",
2115
+ "pane_id": "%1",
2116
+ "pane_pid": 101,
2117
+ "window": "f1",
2118
+ "spawned_at": "2026-08-28T00:00:01Z",
2119
+ "spawn_epoch": 1
2120
+ }
2121
+ }
2122
+ })
2123
+ };
2124
+ let f2 = json!({
2125
+ "agent_id": "f2",
2126
+ "provider": "codex",
2127
+ "role": "Developer",
2128
+ "model": "gpt-5.5",
2129
+ "status": "running",
2130
+ "session_id": "session-f2",
2131
+ "rollout_path": "/tmp/f2.jsonl",
2132
+ "captured_at": "2026-08-28T00:00:02Z",
2133
+ "captured_via": "contract-fixture",
2134
+ "attribution_confidence": "high",
2135
+ "capture_state": "complete",
2136
+ "spawn_cwd": "/tmp/f2",
2137
+ "owner_team_id": "team-a",
2138
+ "pane_id": "%2",
2139
+ "pane_pid": 102,
2140
+ "window": "f2",
2141
+ "spawned_at": "2026-08-28T00:00:03Z",
2142
+ "spawn_epoch": 1
2143
+ });
2144
+
2145
+ // A stale f2 finalization omitted the peer that finalized after its
2146
+ // snapshot. Lifecycle authority preserves f1 byte-for-byte and writes f2.
2147
+ let ws = temp_ws();
2148
+ let latest = latest_state();
2149
+ write_state(&ws, &latest);
2150
+ let incoming = json!({
2151
+ "session_name": "team-a",
2152
+ "active_team_key": "team-a",
2153
+ "agents": {"f2": f2.clone()}
2154
+ });
2155
+ save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
2156
+ .unwrap();
2157
+ let saved = read_state(&ws);
2158
+ assert_eq!(saved["agents"]["f1"], latest["agents"]["f1"]);
2159
+ assert_eq!(saved["agents"]["f2"], f2);
2160
+
2161
+ // Captured R4 f1/f2 shape: the writer includes a stale, already
2162
+ // running f1 snapshot while finalizing f2. Lifecycle authority must
2163
+ // preserve f1 byte-for-byte rather than report SaveConflict for its
2164
+ // five topology fields.
2165
+ let ws = temp_ws();
2166
+ let latest = latest_state();
2167
+ write_state(&ws, &latest);
2168
+ let incoming = json!({
2169
+ "session_name": "team-a",
2170
+ "active_team_key": "team-a",
2171
+ "agents": {
2172
+ "f1": {
2173
+ "agent_id": "f1",
2174
+ "provider": "codex",
2175
+ "status": "running",
2176
+ "session_id": "session-f1",
2177
+ "rollout_path": "/tmp/f1.jsonl",
2178
+ "captured_at": "2026-08-28T00:00:00Z",
2179
+ "captured_via": "contract-fixture",
2180
+ "attribution_confidence": "high",
2181
+ "capture_state": "complete",
2182
+ "spawn_cwd": "/tmp/f1",
2183
+ "owner_team_id": "team-a",
2184
+ "pane_id": "%stale",
2185
+ "pane_pid": 999,
2186
+ "window": "f1-stale",
2187
+ "spawned_at": "2026-08-27T23:59:59Z",
2188
+ "spawn_epoch": 0
2189
+ },
2190
+ "f2": f2.clone()
2191
+ }
2192
+ });
2193
+ save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
2194
+ .unwrap();
2195
+ let saved = read_state(&ws);
2196
+ assert_eq!(saved["agents"]["f1"], latest["agents"]["f1"]);
2197
+ assert_eq!(saved["agents"]["f2"], f2);
2198
+
2199
+ // The same omission without lifecycle authority remains a conflict.
2200
+ let ws = temp_ws();
2201
+ let latest = latest_state();
2202
+ write_state(&ws, &latest);
2203
+ let incoming = json!({
2204
+ "session_name": "team-a",
2205
+ "active_team_key": "team-a",
2206
+ "agents": {"f2": f2.clone()}
2207
+ });
2208
+ assert!(matches!(
2209
+ save_runtime_state(&ws, &incoming),
2210
+ Err(StateError::SaveConflict(_))
2211
+ ));
2212
+ assert_eq!(read_state(&ws), latest);
2213
+
2214
+ // Explicit deletion remains authoritative and must not be resurrected.
2215
+ let ws = temp_ws();
2216
+ let latest = latest_state();
2217
+ write_state(&ws, &latest);
2218
+ let incoming = json!({
2219
+ "session_name": "team-a",
2220
+ "active_team_key": "team-a",
2221
+ "agents": {"f2": f2}
2222
+ });
2223
+ save_runtime_state_with_deleted_agents(&ws, &incoming, &["f1"]).unwrap();
2224
+ let saved = read_state(&ws);
2225
+ assert!(saved["agents"].get("f1").is_none());
2226
+ assert!(saved["agents"].get("f2").is_some());
2227
+ }
2228
+
2229
+ #[test]
2230
+ fn lifecycle_authority_preserves_complete_latest_row_for_starting_peer() {
2231
+ let ws = temp_ws();
2232
+ let latest = json!({
2233
+ "session_name": "team-a",
2234
+ "active_team_key": "team-a",
2235
+ "agents": {
2236
+ "f1": {
2237
+ "agent_id": "f1",
2238
+ "provider": "codex",
2239
+ "status": "running",
2240
+ "session_id": "session-f1",
2241
+ "rollout_path": "/tmp/f1.jsonl",
2242
+ "captured_at": "2026-08-28T00:00:00Z",
2243
+ "captured_via": "contract-fixture",
2244
+ "attribution_confidence": "high",
2245
+ "capture_state": "complete",
2246
+ "spawn_cwd": "/tmp/f1",
2247
+ "owner_team_id": "team-a",
2248
+ "pane_id": "%1",
2249
+ "pane_pid": 101,
2250
+ "window": "f1",
2251
+ "spawned_at": "2026-08-28T00:00:01Z",
2252
+ "spawn_epoch": 1
2253
+ }
2254
+ }
2255
+ });
2256
+ write_state(&ws, &latest);
2257
+ let incoming = json!({
2258
+ "session_name": "team-a",
2259
+ "active_team_key": "team-a",
2260
+ "agents": {
2261
+ "f1": {
2262
+ "agent_id": "f1",
2263
+ "provider": "codex",
2264
+ "status": "starting",
2265
+ "stale_metadata": "must-not-survive"
2266
+ },
2267
+ "f2": {"agent_id": "f2", "status": "starting"}
2268
+ }
2269
+ });
2270
+ save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
2271
+ .unwrap();
2272
+ assert_eq!(read_state(&ws)["agents"]["f1"], latest["agents"]["f1"]);
2273
+ }
2274
+
2275
+ #[test]
2276
+ fn lifecycle_authority_still_conflicts_on_different_completed_topology() {
2277
+ let ws = temp_ws();
2278
+ let latest = json!({
2279
+ "session_name": "team-a",
2280
+ "active_team_key": "team-a",
2281
+ "agents": {
2282
+ "f1": {
2283
+ "agent_id": "f1",
2284
+ "status": "running",
2285
+ "pane_id": "%1",
2286
+ "pane_pid": 101,
2287
+ "window": "f1",
2288
+ "spawned_at": "2026-08-28T00:00:01Z",
2289
+ "spawn_epoch": 1
2290
+ }
2291
+ }
2292
+ });
2293
+ write_state(&ws, &latest);
2294
+ let incoming = json!({
2295
+ "session_name": "team-a",
2296
+ "active_team_key": "team-a",
2297
+ "agents": {
2298
+ "f1": {
2299
+ "agent_id": "f1",
2300
+ "status": "running",
2301
+ "pane_id": "%9",
2302
+ "pane_pid": 109,
2303
+ "window": "f1-new",
2304
+ "spawned_at": "2026-08-28T00:00:09Z",
2305
+ "spawn_epoch": 9
2306
+ },
2307
+ "f2": {"agent_id": "f2", "status": "running"}
2308
+ }
2309
+ });
2310
+ let err =
2311
+ save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
2312
+ .expect_err("two completed topologies for f1 must conflict");
2313
+ assert!(matches!(err, StateError::SaveConflict(_)));
2314
+ assert!(err.to_string().contains("agent_id=f1"));
2315
+ }
2316
+
2024
2317
  #[test]
2025
2318
  fn owner_epoch_preserve_is_explicit_and_bounded_to_team_entry() {
2026
2319
  let ws = temp_ws();
@@ -235,6 +235,7 @@ pub enum StateWriteIntent<'a> {
235
235
  MessagingTurnArm {
236
236
  owner_team_id: Option<&'a str>,
237
237
  },
238
+ MessagingSessionCapture,
238
239
  ResultCollection {
239
240
  owner_team_id: Option<&'a str>,
240
241
  },
@@ -415,6 +416,10 @@ fn route_direct(
415
416
  },
416
417
  // MessagingTurnArm -> root save (messaging/activity.rs:83).
417
418
  StateWriteIntent::MessagingTurnArm { .. } => helper_write_root(workspace, state),
419
+ // MessagingSessionCapture -> root save; the post-send capture backfill
420
+ // at messaging/send.rs used the root helper directly before it was
421
+ // migrated onto the repository.
422
+ StateWriteIntent::MessagingSessionCapture => helper_write_root(workspace, state),
418
423
  // ResultCollection direct-write path uses `save_team_scoped_state`
419
424
  // when an owner_team_id is bound and `save_runtime_state` at the root
420
425
  // otherwise (messaging/results.rs:201/211). The reapply-shaped