@team-agent/installer 0.5.70 → 0.5.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.70"
584
+ version = "0.5.71"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.70"
12
+ version = "0.5.71"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -3393,6 +3393,40 @@ pub mod lifecycle_port {
3393
3393
  Some(crate::cli::QUICK_START_REMINDER)
3394
3394
  );
3395
3395
  }
3396
+
3397
+ #[test]
3398
+ fn spawn_failure_without_session_does_not_claim_a_worker_pane_exists() {
3399
+ let value = restart_value(
3400
+ crate::lifecycle::RestartReport::Failed {
3401
+ session_name: crate::transport::SessionName::new("team-current"),
3402
+ failed_agents: vec![crate::lifecycle::RestartFailedAgent {
3403
+ agent_id: crate::model::ids::AgentId::new("coder"),
3404
+ restart_mode: crate::lifecycle::StartMode::Fresh,
3405
+ decision: crate::lifecycle::ResumeDecision::FreshStart,
3406
+ session_id: None,
3407
+ phase: "spawn".to_string(),
3408
+ error: "provider io error".to_string(),
3409
+ }],
3410
+ next_actions: Vec::new(),
3411
+ attach_commands: Vec::new(),
3412
+ },
3413
+ None,
3414
+ );
3415
+ let action = value
3416
+ .pointer("/failed_agents/0/action")
3417
+ .and_then(Value::as_str)
3418
+ .expect("spawn failure action");
3419
+
3420
+ assert!(
3421
+ action.contains("before a pane was created"),
3422
+ "action={action}"
3423
+ );
3424
+ assert!(
3425
+ !action.contains("inspect worker coder output"),
3426
+ "action={action}"
3427
+ );
3428
+ assert!(!action.contains("restart-agent"), "action={action}");
3429
+ }
3396
3430
  }
3397
3431
 
3398
3432
  fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
@@ -3436,10 +3470,10 @@ pub mod lifecycle_port {
3436
3470
  "session_id": failure.session_id.as_ref().map(|session| session.as_str()),
3437
3471
  "phase": failure.phase,
3438
3472
  "error": failure.error,
3439
- "action": format!(
3440
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
3441
- failure.agent_id,
3442
- failure.agent_id
3473
+ "action": crate::lifecycle::restart_failure_action(
3474
+ &failure.agent_id,
3475
+ &failure.phase,
3476
+ failure.session_id.as_ref(),
3443
3477
  ),
3444
3478
  "log": format!(
3445
3479
  ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
@@ -3470,10 +3504,10 @@ pub mod lifecycle_port {
3470
3504
  "session_id": failure.session_id.as_ref().map(|session| session.as_str()),
3471
3505
  "phase": failure.phase,
3472
3506
  "error": failure.error,
3473
- "action": format!(
3474
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
3475
- failure.agent_id,
3476
- failure.agent_id
3507
+ "action": crate::lifecycle::restart_failure_action(
3508
+ &failure.agent_id,
3509
+ &failure.phase,
3510
+ failure.session_id.as_ref(),
3477
3511
  ),
3478
3512
  "log": format!(
3479
3513
  ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
@@ -450,13 +450,24 @@ pub(crate) fn validate_pi_wrapper_source(
450
450
  /// errors: identity、序列化、目录、写盘或 rename 失败时返回 ProviderError
451
451
  /// ---
452
452
  pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf, ProviderError> {
453
+ write_pi_wrapper_with_publish(request, |from, to| std::fs::rename(from, to))
454
+ }
455
+
456
+ pub(crate) fn write_pi_wrapper_with_publish(
457
+ request: PiWrapperRequest<'_>,
458
+ publish: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
459
+ ) -> Result<PathBuf, ProviderError> {
453
460
  let source = render_pi_wrapper(&request)?;
454
461
  validate_pi_wrapper_source(&source, request.adapter, request.candidate_executable)?;
455
462
  let parent = request.destination.parent().ok_or_else(|| {
456
463
  ProviderError::Io("Pi wrapper destination has no parent directory".to_string())
457
464
  })?;
458
- std::fs::create_dir_all(parent)
459
- .map_err(|error| ProviderError::Io(format!("{}: {error}", parent.display())))?;
465
+ std::fs::create_dir_all(parent).map_err(|error| {
466
+ ProviderError::Io(format!(
467
+ "Pi wrapper atomic write stage=create_parent path={}: {error}",
468
+ parent.display()
469
+ ))
470
+ })?;
460
471
  let temp = parent.join(format!(".team-mcp.ts.{}.tmp", std::process::id()));
461
472
  let write_result = (|| {
462
473
  let mut file = OpenOptions::new()
@@ -464,22 +475,38 @@ pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf,
464
475
  .write(true)
465
476
  .mode(0o600)
466
477
  .open(&temp)
467
- .map_err(|error| ProviderError::Io(format!("{}: {error}", temp.display())))?;
468
- file.write_all(source.as_bytes())
469
- .map_err(|error| ProviderError::Io(format!("{}: {error}", temp.display())))?;
470
- file.sync_all()
471
- .map_err(|error| ProviderError::Io(format!("{}: {error}", temp.display())))?;
472
- std::fs::rename(&temp, request.destination).map_err(|error| {
478
+ .map_err(|error| {
479
+ ProviderError::Io(format!(
480
+ "Pi wrapper atomic write stage=open_temp path={}: {error}",
481
+ temp.display()
482
+ ))
483
+ })?;
484
+ file.write_all(source.as_bytes()).map_err(|error| {
473
485
  ProviderError::Io(format!(
474
- "rename {} to {}: {error}",
475
- temp.display(),
476
- request.destination.display()
486
+ "Pi wrapper atomic write stage=write_temp path={}: {error}",
487
+ temp.display()
477
488
  ))
478
489
  })?;
479
- std::fs::set_permissions(request.destination, std::fs::Permissions::from_mode(0o600))
490
+ file.set_permissions(std::fs::Permissions::from_mode(0o600))
480
491
  .map_err(|error| {
481
- ProviderError::Io(format!("{}: {error}", request.destination.display()))
492
+ ProviderError::Io(format!(
493
+ "Pi wrapper atomic write stage=chmod_temp_fd path={}: {error}",
494
+ temp.display()
495
+ ))
482
496
  })?;
497
+ file.sync_all().map_err(|error| {
498
+ ProviderError::Io(format!(
499
+ "Pi wrapper atomic write stage=fsync_temp path={}: {error}",
500
+ temp.display()
501
+ ))
502
+ })?;
503
+ publish(&temp, request.destination).map_err(|error| {
504
+ ProviderError::Io(format!(
505
+ "Pi wrapper atomic write stage=rename_publish from={} to={}: {error}",
506
+ temp.display(),
507
+ request.destination.display()
508
+ ))
509
+ })?;
483
510
  Ok(request.destination.to_path_buf())
484
511
  })();
485
512
  if write_result.is_err() {
@@ -2522,10 +2522,10 @@ fn write_restart_agent_failed_event(
2522
2522
  "session_id": decision.session_id.as_ref().map(|session| session.as_str()),
2523
2523
  "phase": phase,
2524
2524
  "error": error,
2525
- "action": format!(
2526
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
2527
- decision.agent_id,
2528
- decision.agent_id
2525
+ "action": restart_failure_action(
2526
+ &decision.agent_id,
2527
+ phase,
2528
+ decision.session_id.as_ref(),
2529
2529
  ),
2530
2530
  "log": format!(
2531
2531
  ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
@@ -3008,14 +3008,30 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
3008
3008
  failed_agents
3009
3009
  .iter()
3010
3010
  .map(|failure| {
3011
- format!(
3012
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
3013
- failure.agent_id, failure.agent_id
3011
+ restart_failure_action(
3012
+ &failure.agent_id,
3013
+ &failure.phase,
3014
+ failure.session_id.as_ref(),
3014
3015
  )
3015
3016
  })
3016
3017
  .collect()
3017
3018
  }
3018
3019
 
3020
+ pub(crate) fn restart_failure_action(
3021
+ agent_id: &AgentId,
3022
+ phase: &str,
3023
+ session_id: Option<&SessionId>,
3024
+ ) -> String {
3025
+ if phase == "spawn" && session_id.is_none() {
3026
+ return format!(
3027
+ "worker {agent_id} failed before a pane was created; inspect the reported provider error, then rerun `team-agent restart --allow-fresh`"
3028
+ );
3029
+ }
3030
+ format!(
3031
+ "inspect worker {agent_id} output, then reset that worker with `team-agent reset-agent {agent_id}` or rerun `team-agent restart --allow-fresh`"
3032
+ )
3033
+ }
3034
+
3019
3035
  fn start_mode_wire(mode: StartMode) -> &'static str {
3020
3036
  match mode {
3021
3037
  StartMode::Resumed => "resumed",
@@ -92,7 +92,8 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
92
92
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
93
93
  // can emit the same instrumentation event family with `source="launch"`.
94
94
  pub(crate) use rebuild::{
95
- provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
95
+ provider_wire_from_state, restart_failure_action, write_worker_spawn_timing_event,
96
+ RestartPhaseTimer,
96
97
  };
97
98
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
98
99
  pub(crate) use rebuild::{
@@ -3,9 +3,10 @@ use std::path::{Path, PathBuf};
3
3
  use std::sync::atomic::{AtomicU32, Ordering};
4
4
 
5
5
  use crate::lifecycle::launch::pi_mcp::{
6
- parse_pi_list_models_table, resolve_pi_executable_chain, validate_pi_adapter_identity,
7
- validate_pi_executable_chain, validate_pi_wrapper_source, write_pi_wrapper, PiAdapterIdentity,
8
- PiExecutableChain, PiExecutableFileType, PiWrapperRequest,
6
+ parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
7
+ validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
8
+ write_pi_wrapper, write_pi_wrapper_with_publish, PiAdapterIdentity, PiExecutableChain,
9
+ PiExecutableFileType, PiWrapperRequest,
9
10
  };
10
11
  use crate::provider::McpConfig;
11
12
 
@@ -336,6 +337,124 @@ fn pi_wrapper_is_atomic_body(hermetic: &HermeticTestEnv) {
336
337
  std::fs::remove_dir_all(root).expect("remove wrapper fixture");
337
338
  }
338
339
 
340
+ #[test]
341
+ fn pi_leader_wrapper_materializes_from_empty_unicode_wsl_workspace() {
342
+ let root = temp_root("empty-unicode-leader");
343
+ let workspace = root.join("mnt/f/测试基础设施");
344
+ let candidate = root.join("candidate/team-agent");
345
+ std::fs::create_dir_all(&workspace).expect("create empty Unicode workspace");
346
+ std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
347
+ .expect("create candidate parent");
348
+ std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
349
+ assert!(
350
+ !workspace.join(".team").exists(),
351
+ "fixture must begin without a .team directory"
352
+ );
353
+
354
+ let paths = pi_seat_paths(&workspace, "current", "leader");
355
+ let adapter = adapter_identity(&root);
356
+ let written = write_pi_wrapper(PiWrapperRequest {
357
+ destination: &paths.wrapper,
358
+ adapter: &adapter,
359
+ candidate_executable: &candidate,
360
+ mcp_config: &mcp_config(&candidate, &workspace, "leader"),
361
+ team_id: "current",
362
+ agent_id: "leader",
363
+ workspace: &workspace,
364
+ include_tools: &["assign_task", "send_message"],
365
+ })
366
+ .expect("materialize Pi leader wrapper from an empty Unicode workspace");
367
+
368
+ assert_eq!(written, paths.wrapper);
369
+ assert!(written.is_file(), "published leader wrapper must exist");
370
+ assert!(
371
+ paths.runtime_root.is_dir(),
372
+ "leader runtime root must be created recursively"
373
+ );
374
+ let source = std::fs::read_to_string(&written).expect("read complete leader wrapper");
375
+ validate_pi_wrapper_source(&source, &adapter, &candidate)
376
+ .expect("published leader wrapper must be complete and valid");
377
+ assert!(source.contains("TEAM_AGENT_ID") && source.contains("leader"));
378
+ assert!(source.contains("TEAM_AGENT_OWNER_TEAM_ID") && source.contains("current"));
379
+ std::fs::remove_dir_all(root).expect("remove Unicode leader fixture");
380
+ }
381
+
382
+ #[test]
383
+ fn pi_wrapper_permissions_are_finalized_before_atomic_publish() {
384
+ let source = include_str!("../launch/pi_mcp.rs");
385
+ let write_start = source
386
+ .find("pub(crate) fn write_pi_wrapper")
387
+ .expect("write_pi_wrapper source");
388
+ let write_source = &source[write_start..];
389
+ let permissions = write_source
390
+ .find("file.set_permissions")
391
+ .expect("wrapper permissions must use the open temp-file handle");
392
+ let publish = write_source
393
+ .find("publish(&temp, request.destination)")
394
+ .expect("atomic wrapper publish");
395
+
396
+ assert!(
397
+ permissions < publish,
398
+ "DrvFS may not resolve the destination immediately after rename; finalize mode through the open temp-file handle before publishing"
399
+ );
400
+ assert!(
401
+ !write_source.contains("std::fs::set_permissions(request.destination"),
402
+ "atomic publish must not require a second destination-path lookup"
403
+ );
404
+ }
405
+
406
+ #[test]
407
+ fn pi_wrapper_publish_needs_no_final_path_metadata_lookup() {
408
+ let root = temp_root("publish-path-enoent");
409
+ let workspace = root.join("mnt/f/测试基础设施");
410
+ let candidate = root.join("candidate/team-agent");
411
+ std::fs::create_dir_all(&workspace).expect("create Unicode workspace");
412
+ std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
413
+ .expect("create candidate parent");
414
+ std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
415
+
416
+ let paths = pi_seat_paths(&workspace, "current", "leader");
417
+ let published_parent = paths.runtime_root.clone();
418
+ let hidden_parent = published_parent.with_file_name("leader-after-rename");
419
+ let adapter = adapter_identity(&root);
420
+ write_pi_wrapper_with_publish(
421
+ PiWrapperRequest {
422
+ destination: &paths.wrapper,
423
+ adapter: &adapter,
424
+ candidate_executable: &candidate,
425
+ mcp_config: &mcp_config(&candidate, &workspace, "leader"),
426
+ team_id: "current",
427
+ agent_id: "leader",
428
+ workspace: &workspace,
429
+ include_tools: &["assign_task", "send_message"],
430
+ },
431
+ |temp, destination| {
432
+ std::fs::rename(temp, destination)?;
433
+ std::fs::rename(&published_parent, &hidden_parent)?;
434
+ assert_eq!(
435
+ std::fs::metadata(destination)
436
+ .expect_err("simulate DrvFS final-path metadata ENOENT after rename")
437
+ .kind(),
438
+ std::io::ErrorKind::NotFound
439
+ );
440
+ Ok(())
441
+ },
442
+ )
443
+ .expect("successful publish must not perform final-path metadata operations");
444
+
445
+ let published = hidden_parent.join("team-mcp.ts");
446
+ assert!(published.is_file(), "rename published a complete wrapper");
447
+ assert_eq!(
448
+ std::fs::metadata(&published)
449
+ .expect("published wrapper metadata through converged path")
450
+ .permissions()
451
+ .mode()
452
+ & 0o777,
453
+ 0o600
454
+ );
455
+ std::fs::remove_dir_all(root).expect("remove publish-path fixture");
456
+ }
457
+
339
458
  #[test]
340
459
  fn pi_wrapper_runtime_registration_does_not_copy_ambient_config() {
341
460
  run_process_isolated(PI_WRAPPER_AMBIENT_CHILD, PI_WRAPPER_AMBIENT_TEST, || {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.70",
3
+ "version": "0.5.71",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.70",
24
- "@team-agent/cli-darwin-x64": "0.5.70",
25
- "@team-agent/cli-linux-x64": "0.5.70"
23
+ "@team-agent/cli-darwin-arm64": "0.5.71",
24
+ "@team-agent/cli-darwin-x64": "0.5.71",
25
+ "@team-agent/cli-linux-x64": "0.5.71"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -2,7 +2,7 @@
2
2
  name: team-agent
3
3
  description: Use only when the user explicitly asks to start, operate, inspect, shutdown, or restart a Team Agent team. Treat the team-agent CLI as a sealed appliance.
4
4
  requires_team_agent: ">=0.5.0"
5
- last_verified_against: "0.5.70"
5
+ last_verified_against: "0.5.71"
6
6
  ---
7
7
 
8
8
  # Team Agent