@team-agent/installer 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/Cargo.lock +3 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +20 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +191 -31
  5. package/crates/team-agent/src/cli/emit.rs +5 -0
  6. package/crates/team-agent/src/cli/mod.rs +213 -89
  7. package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
  8. package/crates/team-agent/src/codex_app_server.rs +62 -1
  9. package/crates/team-agent/src/conpty/backend.rs +120 -8
  10. package/crates/team-agent/src/coordinator/backoff.rs +88 -2
  11. package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
  12. package/crates/team-agent/src/coordinator/health.rs +97 -11
  13. package/crates/team-agent/src/coordinator/mod.rs +8 -0
  14. package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
  15. package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
  16. package/crates/team-agent/src/coordinator/tick.rs +13 -0
  17. package/crates/team-agent/src/diagnose/orphans.rs +18 -7
  18. package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
  19. package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
  20. package/crates/team-agent/src/lib.rs +14 -1
  21. package/crates/team-agent/src/lifecycle/launch.rs +89 -92
  22. package/crates/team-agent/src/lifecycle/lock.rs +40 -33
  23. package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
  24. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
  26. package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
  27. package/crates/team-agent/src/lifecycle/restart.rs +16 -6
  28. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
  29. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
  30. package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
  31. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  32. package/crates/team-agent/src/mcp_server/wire.rs +6 -7
  33. package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
  34. package/crates/team-agent/src/packaging/tests.rs +41 -6
  35. package/crates/team-agent/src/packaging/types.rs +31 -3
  36. package/crates/team-agent/src/platform/argv.rs +324 -0
  37. package/crates/team-agent/src/platform/errors.rs +95 -0
  38. package/crates/team-agent/src/platform/file_lock.rs +418 -0
  39. package/crates/team-agent/src/platform/mod.rs +66 -0
  40. package/crates/team-agent/src/platform/process.rs +555 -0
  41. package/crates/team-agent/src/provider/adapter.rs +103 -41
  42. package/crates/team-agent/src/provider/session/capture.rs +328 -66
  43. package/crates/team-agent/src/provider/session/resume.rs +30 -28
  44. package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
  45. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  46. package/crates/team-agent/src/provider/session_scan.rs +1 -0
  47. package/crates/team-agent/src/state/persist.rs +222 -25
  48. package/crates/team-agent/src/state/projection.rs +59 -4
  49. package/crates/team-agent/src/tmux_backend.rs +63 -13
  50. package/crates/team-agent/src/transport_factory.rs +124 -5
  51. package/package.json +4 -4
@@ -17,6 +17,7 @@ use super::StateError;
17
17
  use crate::state::persist::{
18
18
  load_runtime_state, save_runtime_state_with_deleted_agents,
19
19
  save_runtime_state_with_lifecycle_topology_authority,
20
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip,
20
21
  save_runtime_state_with_team_tombstone_lifecycle_topology_authority,
21
22
  save_runtime_state_with_team_tombstoned_agents,
22
23
  };
@@ -595,7 +596,7 @@ pub(crate) fn save_team_scoped_state_with_tombstone_lifecycle_topology_authority
595
596
  team_state: &Value,
596
597
  agent_ids: &[&str],
597
598
  ) -> Result<(), StateError> {
598
- save_team_scoped_state_with_merge_options(workspace, team_state, &[], agent_ids, agent_ids)
599
+ save_team_scoped_state_with_merge_options(workspace, team_state, &[], agent_ids, &[], agent_ids)
599
600
  }
600
601
 
601
602
  pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority(
@@ -603,7 +604,23 @@ pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority(
603
604
  team_state: &Value,
604
605
  agent_ids: &[&str],
605
606
  ) -> Result<(), StateError> {
606
- save_team_scoped_state_with_merge_options(workspace, team_state, &[], &[], agent_ids)
607
+ save_team_scoped_state_with_merge_options(workspace, team_state, &[], &[], &[], agent_ids)
608
+ }
609
+
610
+ pub(crate) fn save_team_scoped_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
611
+ workspace: &Path,
612
+ team_state: &Value,
613
+ skip_capture_backfill_agent_ids: &[&str],
614
+ topology_agent_ids: &[&str],
615
+ ) -> Result<(), StateError> {
616
+ save_team_scoped_state_with_merge_options(
617
+ workspace,
618
+ team_state,
619
+ &[],
620
+ &[],
621
+ skip_capture_backfill_agent_ids,
622
+ topology_agent_ids,
623
+ )
607
624
  }
608
625
 
609
626
  fn save_team_scoped_state_with_merge_exceptions(
@@ -618,6 +635,7 @@ fn save_team_scoped_state_with_merge_exceptions(
618
635
  deleted_agent_ids,
619
636
  tombstoned_agent_ids,
620
637
  &[],
638
+ &[],
621
639
  )
622
640
  }
623
641
 
@@ -626,6 +644,7 @@ fn save_team_scoped_state_with_merge_options(
626
644
  team_state: &Value,
627
645
  deleted_agent_ids: &[&str],
628
646
  tombstoned_agent_ids: &[&str],
647
+ skip_capture_backfill_agent_ids: &[&str],
629
648
  topology_agent_ids: &[&str],
630
649
  ) -> Result<(), StateError> {
631
650
  let target_key = team_state_key(team_state);
@@ -669,12 +688,30 @@ fn save_team_scoped_state_with_merge_options(
669
688
  topology_agent_ids,
670
689
  );
671
690
  }
691
+ if !skip_capture_backfill_agent_ids.is_empty() {
692
+ return save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
693
+ workspace,
694
+ &merged,
695
+ &target_key,
696
+ skip_capture_backfill_agent_ids,
697
+ topology_agent_ids,
698
+ );
699
+ }
672
700
  return save_runtime_state_with_lifecycle_topology_authority(
673
701
  workspace,
674
702
  &merged,
675
703
  topology_agent_ids,
676
704
  );
677
705
  }
706
+ if !skip_capture_backfill_agent_ids.is_empty() {
707
+ return save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
708
+ workspace,
709
+ &merged,
710
+ &target_key,
711
+ skip_capture_backfill_agent_ids,
712
+ &[],
713
+ );
714
+ }
678
715
  if tombstoned_agent_ids.is_empty() {
679
716
  return save_runtime_state_with_deleted_agents(workspace, &merged, deleted_agent_ids);
680
717
  }
@@ -709,10 +746,28 @@ fn save_team_scoped_state_with_merge_options(
709
746
  }
710
747
  if tombstoned_agent_ids.is_empty() {
711
748
  if !topology_agent_ids.is_empty() {
712
- save_runtime_state_with_lifecycle_topology_authority(
749
+ if !skip_capture_backfill_agent_ids.is_empty() {
750
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
751
+ workspace,
752
+ &Value::Object(merged),
753
+ &target_key,
754
+ skip_capture_backfill_agent_ids,
755
+ topology_agent_ids,
756
+ )
757
+ } else {
758
+ save_runtime_state_with_lifecycle_topology_authority(
759
+ workspace,
760
+ &Value::Object(merged),
761
+ topology_agent_ids,
762
+ )
763
+ }
764
+ } else if !skip_capture_backfill_agent_ids.is_empty() {
765
+ save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
713
766
  workspace,
714
767
  &Value::Object(merged),
715
- topology_agent_ids,
768
+ &target_key,
769
+ skip_capture_backfill_agent_ids,
770
+ &[],
716
771
  )
717
772
  } else {
718
773
  save_runtime_state_with_deleted_agents(
@@ -18,6 +18,13 @@
18
18
  use std::collections::BTreeMap;
19
19
  use std::hash::{Hash, Hasher};
20
20
  use std::io::{Read, Write};
21
+ // 0.5.x Windows portability Batch 1: gate the sole Unix API surface
22
+ // in this concrete tmux backend (FileTypeExt::is_socket + libc::geteuid
23
+ // tmux-socket-root derivation). The rest of the file is Command::new
24
+ // "tmux" shellouts that compile on Windows but return runtime errors
25
+ // honestly (tmux binary absent → typed subprocess error).
26
+ // Truth source: `.team/artifacts/0.5.x-windows-portability-survey-design.md` §Batch 1.
27
+ #[cfg(unix)]
21
28
  use std::os::unix::fs::FileTypeExt;
22
29
  use std::path::{Path, PathBuf};
23
30
  use std::process::Stdio;
@@ -489,10 +496,24 @@ pub(crate) fn socket_path_for_name(socket_name: &str) -> Option<PathBuf> {
489
496
  if let Some(existing) = existing_socket_path_for_name(socket_name) {
490
497
  return Some(existing);
491
498
  }
492
- let uid = unsafe { libc::geteuid() };
493
- let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
494
- let default_root = default_root.canonicalize().unwrap_or(default_root);
495
- Some(default_root.join(socket_name))
499
+ // Batch 1: tmux uses `/tmp/tmux-<uid>/` on Unix; on Windows this
500
+ // helper is dead code (Command::new "tmux" fails at spawn time
501
+ // before this path is dereferenced). N38 typed unsupported honored
502
+ // at the shellout boundary.
503
+ #[cfg(unix)]
504
+ {
505
+ let uid = unsafe { libc::geteuid() };
506
+ let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
507
+ let default_root = default_root.canonicalize().unwrap_or(default_root);
508
+ Some(default_root.join(socket_name))
509
+ }
510
+ #[cfg(not(unix))]
511
+ {
512
+ // Windows: tmux socket-root derivation is meaningless. Return
513
+ // None so the caller sees the honest "no such socket" branch.
514
+ let _ = socket_name;
515
+ None
516
+ }
496
517
  }
497
518
 
498
519
  fn existing_socket_path_for_name(socket_name: &str) -> Option<PathBuf> {
@@ -582,14 +603,25 @@ pub(crate) fn attach_commands_for_windows<'a>(
582
603
  }
583
604
 
584
605
  pub(crate) fn tmux_socket_roots() -> Vec<PathBuf> {
585
- let uid = unsafe { libc::geteuid() };
586
- let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
587
- if let Some(tmpdir) = std::env::var_os("TMPDIR") {
588
- roots.push(PathBuf::from(tmpdir).join(format!("tmux-{uid}")));
606
+ // Batch 1: `/tmp/tmux-<uid>` root enumeration is Unix-only tmux
607
+ // convention. On Windows return empty so the caller loops zero
608
+ // times (honest "no tmux socket roots" — tmux is not deployed on
609
+ // native Windows without WSL, and WSL is out of scope).
610
+ #[cfg(unix)]
611
+ {
612
+ let uid = unsafe { libc::geteuid() };
613
+ let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
614
+ if let Some(tmpdir) = std::env::var_os("TMPDIR") {
615
+ roots.push(PathBuf::from(tmpdir).join(format!("tmux-{uid}")));
616
+ }
617
+ roots.sort();
618
+ roots.dedup();
619
+ roots
620
+ }
621
+ #[cfg(not(unix))]
622
+ {
623
+ Vec::new()
589
624
  }
590
- roots.sort();
591
- roots.dedup();
592
- roots
593
625
  }
594
626
 
595
627
  pub(crate) fn tmux_socket_endpoints() -> Vec<String> {
@@ -602,7 +634,20 @@ pub(crate) fn tmux_socket_endpoints() -> Vec<String> {
602
634
  let Ok(file_type) = entry.file_type() else {
603
635
  continue;
604
636
  };
605
- if !file_type.is_socket() {
637
+ // Batch 1: `FileTypeExt::is_socket` is Unix-only. Dead code
638
+ // on Windows because `tmux_socket_roots()` returns empty
639
+ // there (no /tmp/tmux-<uid> convention). We still cfg the
640
+ // method call so `cargo check --target x86_64-pc-windows-msvc`
641
+ // sees no `std::os::unix::fs::FileTypeExt` reference.
642
+ #[cfg(unix)]
643
+ {
644
+ if !file_type.is_socket() {
645
+ continue;
646
+ }
647
+ }
648
+ #[cfg(not(unix))]
649
+ {
650
+ let _ = file_type;
606
651
  continue;
607
652
  }
608
653
  let path = entry.path();
@@ -2249,5 +2294,10 @@ fn non_empty(raw: &str) -> Option<&str> {
2249
2294
  }
2250
2295
  }
2251
2296
 
2252
- #[cfg(test)]
2297
+ // 0.5.x Windows portability Batch 5: the `tmux_backend/tests.rs`
2298
+ // module uses `std::os::unix::net::UnixListener` for its mock
2299
+ // runner + verifies Unix-specific socket-root derivation. Since the
2300
+ // tmux backend itself only functions on Unix (design § Route B —
2301
+ // tmux is a Unix concept), the test module stays Unix-only.
2302
+ #[cfg(all(test, unix))]
2253
2303
  mod tests;
@@ -351,12 +351,61 @@ fn build_conpty(
351
351
  // the same source of truth the tmux short-socket derivation uses,
352
352
  // so the shim + tmux socket see identical workspace identity.
353
353
  let workspace_hash = crate::tmux_backend::workspace_short_hash_pub(input.workspace);
354
- // No pipe client is wired here (that's Batch 2/3 scope). Without
355
- // a live client, `ConPtyBackend` degrades to `MuxUnavailable`
356
- // honestly which is exactly what C-1 requires when the caller
357
- // asked for conpty on a host with no shim.
358
- let backend = ConPtyBackend::new(workspace_hash, team_key);
354
+ // `mut` because the Windows branch below may reassign after a
355
+ // successful pipe connect + `with_pipe_client` handshake. On
356
+ // Unix the binding stays as-constructed.
357
+ #[cfg_attr(not(windows), allow(unused_mut))]
358
+ let mut backend = ConPtyBackend::new(workspace_hash.clone(), team_key);
359
359
  let mut notices = Vec::new();
360
+ // 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): factory returns to
361
+ // pure assembly. `build_conpty` used to call
362
+ // `NamedPipeClient::connect(_, 2000)` inline when
363
+ // `conpty_pipe_ready(input)` said state hinted the shim was up.
364
+ // That was a real CR C-5 violation — every `status`/`diagnose`
365
+ // resolve on Windows could block 2 seconds probing a
366
+ // non-existent pipe.
367
+ //
368
+ // Post-fix: the factory only STASHES the pipe hint via
369
+ // `ConPtyBackend::with_pipe_hint(pipe_name)`. The backend's
370
+ // `ensure_pipe_client` performs the connect + Hello lazily on
371
+ // the first Op that needs the pipe. Callers that don't need
372
+ // the pipe (status, resolve-only paths) never pay the connect
373
+ // cost.
374
+ //
375
+ // The Batch 6 direct-handoff path (`lifecycle/launch.rs`
376
+ // quick-start conpty) stays byte-compatible: it hands its
377
+ // Hello-connected client to `with_pipe_client` AFTER
378
+ // `resolve_transport` returns, and the client just becomes the
379
+ // backend's initial cache value so lazy connect is a no-op on
380
+ // the first Op.
381
+ #[cfg(windows)]
382
+ {
383
+ if let Some(state) = input.state {
384
+ if let Some(pipe_name) = state
385
+ .pointer("/transport/shim/pipe_name")
386
+ .and_then(|v| v.as_str())
387
+ {
388
+ backend = backend.with_pipe_hint(pipe_name);
389
+ }
390
+ }
391
+ // Non-state env override still respected (test/diagnostic
392
+ // path). When set, derive the hint from workspace_hash +
393
+ // team_key so `dispatch` can lazily connect the first Op.
394
+ if std::env::var("TEAM_AGENT_FORCE_CONPTY_PIPE_CONNECT")
395
+ .map(|v| v == "1")
396
+ .unwrap_or(false)
397
+ && backend.pipe_hint_ref().is_none()
398
+ {
399
+ let pipe_name = conpty_transport::pipe_name_for(&workspace_hash, team_key);
400
+ backend = backend.with_pipe_hint(pipe_name);
401
+ }
402
+ }
403
+ // Suppress unused warnings on Unix where the cfg(windows) block
404
+ // above compiles to nothing.
405
+ #[cfg(not(windows))]
406
+ {
407
+ let _ = &workspace_hash;
408
+ }
360
409
  // C-3: if state has BOTH `transport.kind=conpty` and a legacy
361
410
  // `tmux_endpoint`, tell the caller so they emit
362
411
  // `transport.legacy_tmux_endpoint_ignored`. Same shape for the
@@ -608,6 +657,76 @@ mod tests {
608
657
  assert!(resolved.notices.is_empty());
609
658
  }
610
659
 
660
+ // 0.5.x Windows portability Batch 5: pipe_ready gate — without a
661
+ // shim.pipe_ready hint (or the force-env override) the factory
662
+ // must NOT attempt a connect. Cross-platform test — the
663
+ // `conpty_pipe_ready` gate is only referenced inside cfg(windows)
664
+ // but the observable behavior (no notice, no wired client) is
665
+ // identical on Unix.
666
+ #[test]
667
+ fn conpty_without_pipe_ready_hint_emits_no_pipe_client_notice() {
668
+ let workspace = ws();
669
+ let state = serde_json::json!({ "transport": { "kind": "conpty" } });
670
+ let input = TransportFactoryInput::new(&workspace, TransportPurpose::LifecycleWorker)
671
+ .with_team_key(Some("team-a"))
672
+ .with_state(Some(&state));
673
+ let resolved = resolve_transport(input).expect("must succeed");
674
+ assert_eq!(resolved.kind, BackendKind::ConPty);
675
+ // No pipe-client notice must appear regardless of platform:
676
+ // on Unix the block is cfg'd out; on Windows the gate is
677
+ // closed because no `transport.shim.pipe_ready = true` in
678
+ // state.
679
+ for notice in &resolved.notices {
680
+ assert_ne!(
681
+ notice.event, "transport.conpty_pipe_client_unwired",
682
+ "no pipe-client notice without a pipe_ready hint"
683
+ );
684
+ }
685
+ }
686
+
687
+ // 0.5.x CR C-5 fix (leader msg_a89b5a03bbf0): factory returns to
688
+ // pure assembly. The `transport.conpty_pipe_client_unwired`
689
+ // notice is REMOVED from `build_conpty` — connect is deferred to
690
+ // the backend's `ensure_pipe_client` lazy path, which surfaces
691
+ // failures as `TransportError::MuxUnavailable` at Op call time
692
+ // (not at resolve time).
693
+ //
694
+ // Cross-platform: resolve NEVER emits pipe-client notices on
695
+ // either platform now. This test locks that invariant so a
696
+ // future revert would fire loudly.
697
+ #[test]
698
+ fn conpty_resolve_emits_no_pipe_client_notice_after_c5_fix() {
699
+ let workspace = ws();
700
+ let state = serde_json::json!({
701
+ "transport": {
702
+ "kind": "conpty",
703
+ "shim": {
704
+ "pipe_name": r"\\.\pipe\team-agent-conpty-test-team-a",
705
+ "pipe_ready": true
706
+ },
707
+ }
708
+ });
709
+ let input = TransportFactoryInput::new(&workspace, TransportPurpose::LifecycleWorker)
710
+ .with_team_key(Some("team-a"))
711
+ .with_state(Some(&state));
712
+ let resolved = resolve_transport(input).expect("must succeed");
713
+ assert_eq!(resolved.kind, BackendKind::ConPty);
714
+ let pipe_notices: Vec<_> = resolved
715
+ .notices
716
+ .iter()
717
+ .filter(|n| n.event == "transport.conpty_pipe_client_unwired")
718
+ .collect();
719
+ assert!(
720
+ pipe_notices.is_empty(),
721
+ "CR C-5 fix: resolve must not emit pipe-client notices \
722
+ (connect deferred to backend lazy path). Found: {:?}",
723
+ pipe_notices
724
+ .iter()
725
+ .map(|n| &n.event)
726
+ .collect::<Vec<_>>()
727
+ );
728
+ }
729
+
611
730
  // Wire-string invariants for RequestedTransportBackend so a rename
612
731
  // in Rust cannot silently drift the CLI accept-list.
613
732
  #[test]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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.2",
24
- "@team-agent/cli-darwin-x64": "0.5.2",
25
- "@team-agent/cli-linux-x64": "0.5.2"
23
+ "@team-agent/cli-darwin-arm64": "0.5.4",
24
+ "@team-agent/cli-darwin-x64": "0.5.4",
25
+ "@team-agent/cli-linux-x64": "0.5.4"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",