@team-agent/installer 0.5.75 → 0.5.77

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 (30) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +23 -96
  6. package/crates/team-agent/src/cli/emit.rs +35 -7
  7. package/crates/team-agent/src/cli/mod.rs +63 -42
  8. package/crates/team-agent/src/cli/models.rs +138 -27
  9. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  10. package/crates/team-agent/src/cli/status_port.rs +8 -0
  11. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  12. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  13. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  15. package/crates/team-agent/src/leader/lease.rs +46 -8
  16. package/crates/team-agent/src/leader/types.rs +4 -0
  17. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +81 -4
  18. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
  19. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  20. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
  21. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
  23. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  24. package/crates/team-agent/src/lifecycle/types.rs +4 -0
  25. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  26. package/crates/team-agent/src/state/paths.rs +17 -2
  27. package/crates/team-agent/src/state/projection.rs +18 -2
  28. package/crates/team-agent/src/state/selector.rs +23 -3
  29. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
  30. package/package.json +4 -4
@@ -653,12 +653,30 @@ pub fn recover_watchers_for_incident(
653
653
  /// row for watcher-backed messages. A `submitted_pending_acceptance` row has
654
654
  /// already crossed the transport boundary and is claim-immutable. Any future
655
655
  /// retry belongs to a separate typed recovery arm, not attach/claim convergence.
656
+ /// Compatibility helper retained for callers that only need the total.
657
+ /// The typed recovery path below is the single writer; this wrapper performs
658
+ /// no second requeue and only maps the first result to `total()`.
656
659
  pub(crate) fn requeue_blocked_leader_messages(
657
660
  store: &MessageStore,
658
661
  event_log: &EventLog,
659
662
  owner_team_id: &TeamKey,
660
663
  claimed_pane_id: &PaneId,
661
664
  ) -> Result<usize, MessagingError> {
665
+ requeue_blocked_leader_messages_with_counts(
666
+ store,
667
+ event_log,
668
+ owner_team_id,
669
+ claimed_pane_id,
670
+ )
671
+ .map(crate::message_store::BlockedLeaderRequeueCounts::total)
672
+ }
673
+
674
+ pub(crate) fn requeue_blocked_leader_messages_with_counts(
675
+ store: &MessageStore,
676
+ event_log: &EventLog,
677
+ owner_team_id: &TeamKey,
678
+ claimed_pane_id: &PaneId,
679
+ ) -> Result<crate::message_store::BlockedLeaderRequeueCounts, MessagingError> {
662
680
  // E6 (0.5.9 offline-mailbox §6.5): also requeue rows that a third-party
663
681
  // sender left in `queued_until_leader_attach` via the leader mailbox.
664
682
  // Same idempotent requeue funnel (row/message_id/leader_notification_log
@@ -690,7 +708,7 @@ pub(crate) fn requeue_blocked_leader_messages(
690
708
  }),
691
709
  )?;
692
710
  }
693
- Ok(requeued)
711
+ Ok(counts)
694
712
  }
695
713
 
696
714
  /// `requeue_delivery_exhausted_watchers`: attach-leader 成功后把已经耗尽投递
@@ -702,12 +720,33 @@ pub(crate) fn requeue_blocked_leader_messages(
702
720
  /// `deliver_pending_messages` never re-picks — the new leader would never see the
703
721
  /// pending notification.
704
722
  pub fn requeue_delivery_exhausted_watchers(
705
- _workspace: &Path,
723
+ workspace: &Path,
706
724
  store: &MessageStore,
707
725
  event_log: &EventLog,
708
726
  owner_team_id: &TeamKey,
709
727
  claimed_pane_id: &PaneId,
710
728
  ) -> Result<Vec<WatcherNotice>, MessagingError> {
729
+ requeue_delivery_exhausted_watchers_with_counts(
730
+ workspace,
731
+ store,
732
+ event_log,
733
+ owner_team_id,
734
+ claimed_pane_id,
735
+ )
736
+ .map(|(notices, _)| notices)
737
+ }
738
+
739
+ /// Attach-leader's typed recovery result. The counts come from the same
740
+ /// owner-team scoped attach-recovery call (the watcher and message updates
741
+ /// remain their existing transactions), so an attach caller can expose only
742
+ /// this operation's debt rather than scanning arbitrary historical events.
743
+ pub(crate) fn requeue_delivery_exhausted_watchers_with_counts(
744
+ _workspace: &Path,
745
+ store: &MessageStore,
746
+ event_log: &EventLog,
747
+ owner_team_id: &TeamKey,
748
+ claimed_pane_id: &PaneId,
749
+ ) -> Result<(Vec<WatcherNotice>, crate::message_store::BlockedLeaderRequeueCounts), MessagingError> {
711
750
  let conn = crate::db::schema::open_db(store.db_path())?;
712
751
  let mut stmt = conn.prepare(
713
752
  "select watcher_id, result_id, status from result_watchers
@@ -748,8 +787,13 @@ pub fn requeue_delivery_exhausted_watchers(
748
787
  )?;
749
788
  }
750
789
  drop(stmt);
751
- let _ = requeue_blocked_leader_messages(store, event_log, owner_team_id, claimed_pane_id)?;
752
- Ok(out)
790
+ let counts = requeue_blocked_leader_messages_with_counts(
791
+ store,
792
+ event_log,
793
+ owner_team_id,
794
+ claimed_pane_id,
795
+ )?;
796
+ Ok((out, counts))
753
797
  }
754
798
 
755
799
  /// `delivered_result_message` (`result_delivery.py:394`):内容级去重 —— 查某 result_id 是否已有
@@ -27,7 +27,7 @@
27
27
  use std::path::{Path, PathBuf};
28
28
 
29
29
  use crate::model::paths::{runtime_dir, runtime_spec_path};
30
- use crate::state::persist::load_runtime_state;
30
+ use crate::state::persist::{load_runtime_state, load_runtime_state_without_migrations};
31
31
  use crate::state::projection::team_state_candidates;
32
32
 
33
33
  /// The identity of a team within a workspace. Carries the *workspace root*
@@ -193,10 +193,25 @@ impl CommandScope {
193
193
  /// On any I/O error the empty case is returned — destructive commands
194
194
  /// will run their own selector and surface the real error.
195
195
  pub fn resolve(workspace: &Path, requested_team: Option<&str>) -> Self {
196
+ Self::resolve_from_state(requested_team, load_runtime_state(workspace))
197
+ }
198
+
199
+ /// Ambiguity gate for read-only commands. Must not migrate or persist state.
200
+ pub fn resolve_readonly(workspace: &Path, requested_team: Option<&str>) -> Self {
201
+ Self::resolve_from_state(
202
+ requested_team,
203
+ load_runtime_state_without_migrations(workspace),
204
+ )
205
+ }
206
+
207
+ fn resolve_from_state(
208
+ requested_team: Option<&str>,
209
+ loaded: Result<serde_json::Value, crate::state::StateError>,
210
+ ) -> Self {
196
211
  if let Some(team) = requested_team.filter(|t| !t.is_empty()) {
197
212
  return Self::Resolved(team.to_string());
198
213
  }
199
- let Ok(state) = load_runtime_state(workspace) else {
214
+ let Ok(state) = loaded else {
200
215
  return Self::EmptyWorkspace;
201
216
  };
202
217
  let alive = team_state_candidates(&state);
@@ -16,7 +16,8 @@ use serde_json::{json, Map, Value};
16
16
 
17
17
  use super::StateError;
18
18
  use crate::state::persist::{
19
- load_runtime_state, save_runtime_state_with_deleted_agents,
19
+ load_runtime_state, load_runtime_state_without_migrations,
20
+ save_runtime_state_with_deleted_agents,
20
21
  save_runtime_state_with_lifecycle_topology_authority,
21
22
  save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip,
22
23
  save_runtime_state_with_team_tombstone_lifecycle_topology_authority,
@@ -504,7 +505,22 @@ pub fn resolve_runtime_team_scope(
504
505
  workspace: &Path,
505
506
  team: Option<&str>,
506
507
  ) -> Result<TeamScopeResolution, StateError> {
507
- let state = load_runtime_state(workspace)?;
508
+ resolve_runtime_team_scope_loaded(load_runtime_state(workspace)?, team)
509
+ }
510
+
511
+ /// Same team selection as [`resolve_runtime_team_scope`], but never migrates or
512
+ /// writes runtime state. Status and other read-only CLI surfaces must use this.
513
+ pub fn resolve_runtime_team_scope_readonly(
514
+ workspace: &Path,
515
+ team: Option<&str>,
516
+ ) -> Result<TeamScopeResolution, StateError> {
517
+ resolve_runtime_team_scope_loaded(load_runtime_state_without_migrations(workspace)?, team)
518
+ }
519
+
520
+ fn resolve_runtime_team_scope_loaded(
521
+ state: Value,
522
+ team: Option<&str>,
523
+ ) -> Result<TeamScopeResolution, StateError> {
508
524
  let alive = team_state_candidates(&state);
509
525
  // Python `if team:` —— 空串 falsy,等同无 team(对抗 P1:此前 Some("") 误入 team 分支,空
510
526
  // team_dir 匹配致歧义/未找到错误串漂移)。
@@ -7,7 +7,9 @@ use serde_json::Value;
7
7
 
8
8
  use crate::model::paths::{canonical_run_workspace, runtime_spec_path, team_workspace};
9
9
  use crate::state::persist::runtime_state_path;
10
- use crate::state::projection::resolve_runtime_team_scope;
10
+ use crate::state::projection::{
11
+ resolve_runtime_team_scope, resolve_runtime_team_scope_readonly, TeamScopeResolution,
12
+ };
11
13
  use crate::state::StateError;
12
14
 
13
15
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -35,6 +37,24 @@ pub fn resolve_active_team(
35
37
  input: &Path,
36
38
  team: Option<&str>,
37
39
  mode: SelectorMode,
40
+ ) -> Result<SelectedTeam, StateError> {
41
+ resolve_active_team_with(input, team, mode, resolve_runtime_team_scope)
42
+ }
43
+
44
+ /// Status-path selector: in-memory projection only, no runtime-state writeback.
45
+ pub fn resolve_active_team_readonly(
46
+ input: &Path,
47
+ team: Option<&str>,
48
+ mode: SelectorMode,
49
+ ) -> Result<SelectedTeam, StateError> {
50
+ resolve_active_team_with(input, team, mode, resolve_runtime_team_scope_readonly)
51
+ }
52
+
53
+ fn resolve_active_team_with(
54
+ input: &Path,
55
+ team: Option<&str>,
56
+ mode: SelectorMode,
57
+ resolve_scope: fn(&Path, Option<&str>) -> Result<TeamScopeResolution, StateError>,
38
58
  ) -> Result<SelectedTeam, StateError> {
39
59
  let explicit_spec = input.join("team.spec.yaml");
40
60
  let (run_workspace, resolved) = if explicit_spec.exists() {
@@ -44,7 +64,7 @@ pub fn resolve_active_team(
44
64
  } else {
45
65
  team_run
46
66
  };
47
- let resolved = resolve_runtime_team_scope(&run, team)?;
67
+ let resolved = resolve_scope(&run, team)?;
48
68
  (run, resolved)
49
69
  } else {
50
70
  let run =
@@ -59,7 +79,7 @@ pub fn resolve_active_team(
59
79
  input.display()
60
80
  )));
61
81
  }
62
- let resolved = resolve_runtime_team_scope(&run, team)?;
82
+ let resolved = resolve_scope(&run, team)?;
63
83
  (run, resolved)
64
84
  };
65
85
  let state = resolved.state;
@@ -0,0 +1,122 @@
1
+ use std::fs::OpenOptions;
2
+ use std::io::{Read, Write};
3
+ use std::os::unix::net::{UnixListener, UnixStream};
4
+ use std::path::{Path, PathBuf};
5
+ use std::process::{Command, Stdio};
6
+ use std::time::Duration;
7
+
8
+ const CHILD_FLAG: &str = "--team-agent-models-timeout-child";
9
+ const LIST_MODELS_FLAG: &str = "--list-models";
10
+ const RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_RECEIPT";
11
+ const MODE_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_MODE";
12
+ const READY_SOCKET_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_READY_SOCKET";
13
+ const STDOUT_MARKER: &[u8] = b"__team_agent_models_timeout_descendant_stdout_v1__\n";
14
+
15
+ fn receipt_path() -> PathBuf {
16
+ std::env::var_os(RECEIPT_ENV)
17
+ .map(PathBuf::from)
18
+ .unwrap_or_else(|| std::process::exit(4))
19
+ }
20
+
21
+ fn append_receipt(path: &Path, key: &str, value: impl std::fmt::Display) -> Result<(), ()> {
22
+ let mut receipt = OpenOptions::new()
23
+ .create(true)
24
+ .append(true)
25
+ .open(path)
26
+ .map_err(|_| ())?;
27
+ writeln!(receipt, "{key}={value}").map_err(|_| ())
28
+ }
29
+
30
+ fn child_main(args: &[String]) {
31
+ if args.len() != 1 || args[0] != CHILD_FLAG {
32
+ std::process::exit(5);
33
+ }
34
+ let receipt = receipt_path();
35
+ let socket = std::env::var_os(READY_SOCKET_ENV)
36
+ .map(PathBuf::from)
37
+ .unwrap_or_else(|| std::process::exit(5));
38
+
39
+ let mut stdout = std::io::stdout();
40
+ if stdout.write_all(STDOUT_MARKER).is_err() || stdout.flush().is_err() {
41
+ std::process::exit(5);
42
+ }
43
+ if append_receipt(&receipt, "child_argv_exact", 1).is_err()
44
+ || append_receipt(&receipt, "descendant_pid", std::process::id()).is_err()
45
+ || append_receipt(&receipt, "descendant_started", 1).is_err()
46
+ || append_receipt(&receipt, "descendant_stdout_inherited", 1).is_err()
47
+ {
48
+ std::process::exit(5);
49
+ }
50
+
51
+ let mut ready = UnixStream::connect(socket).unwrap_or_else(|_| std::process::exit(5));
52
+ if ready.write_all(b"1").is_err() {
53
+ std::process::exit(5);
54
+ }
55
+ std::thread::sleep(Duration::from_secs(2));
56
+ }
57
+
58
+ fn main() {
59
+ let args: Vec<String> = std::env::args().skip(1).collect();
60
+ if args.first().is_some_and(|arg| arg == CHILD_FLAG) {
61
+ child_main(&args);
62
+ return;
63
+ }
64
+ if args.len() != 1 || args[0] != LIST_MODELS_FLAG {
65
+ std::process::exit(2);
66
+ }
67
+
68
+ match std::env::var(MODE_ENV) {
69
+ Ok(mode) if mode == "exit7" => {
70
+ eprintln!("sensitive-token");
71
+ std::process::exit(7);
72
+ }
73
+ Ok(mode) if mode == "oversize" => {
74
+ let mut stdout = std::io::stdout();
75
+ let bytes = [b'x'; 64];
76
+ let _ = stdout.write_all(&bytes);
77
+ let _ = stdout.flush();
78
+ return;
79
+ }
80
+ Ok(mode) if mode == "sleep" => {
81
+ std::thread::sleep(Duration::from_secs(2));
82
+ return;
83
+ }
84
+ Ok(mode) if mode == "descendant" => {}
85
+ Err(std::env::VarError::NotPresent) => {}
86
+ Ok(_) | Err(std::env::VarError::NotUnicode(_)) => std::process::exit(2),
87
+ }
88
+
89
+ let receipt = receipt_path();
90
+ let socket = PathBuf::from(format!("{}.sock", receipt.display()));
91
+ let _ = std::fs::remove_file(&socket);
92
+ let listener = UnixListener::bind(&socket).unwrap_or_else(|_| std::process::exit(4));
93
+ if append_receipt(&receipt, "parent_started_once", 1).is_err()
94
+ || append_receipt(&receipt, "parent_argv_exact", 1).is_err()
95
+ || append_receipt(&receipt, "parent_pid", std::process::id()).is_err()
96
+ || append_receipt(&receipt, "child_spawn_attempts", 1).is_err()
97
+ {
98
+ std::process::exit(4);
99
+ }
100
+
101
+ let executable = std::env::current_exe().unwrap_or_else(|_| std::process::exit(4));
102
+ let child = Command::new(executable)
103
+ .arg(CHILD_FLAG)
104
+ .env(RECEIPT_ENV, &receipt)
105
+ .env(READY_SOCKET_ENV, &socket)
106
+ .stdin(Stdio::null())
107
+ .stdout(Stdio::inherit())
108
+ .stderr(Stdio::null())
109
+ .spawn()
110
+ .unwrap_or_else(|_| std::process::exit(4));
111
+ let (mut ready, _) = listener.accept().unwrap_or_else(|_| std::process::exit(4));
112
+ let mut byte = [0_u8; 1];
113
+ if ready.read_exact(&mut byte).is_err() || byte != [b'1'] {
114
+ std::process::exit(4);
115
+ }
116
+ if append_receipt(&receipt, "child_pid", child.id()).is_err() {
117
+ std::process::exit(4);
118
+ }
119
+ let _ = std::fs::remove_file(&socket);
120
+ // The parent exits successfully without waiting; the child retains the
121
+ // inherited stdout pipe and self-exits after the real reader deadline.
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.75",
3
+ "version": "0.5.77",
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.75",
24
- "@team-agent/cli-darwin-x64": "0.5.75",
25
- "@team-agent/cli-linux-x64": "0.5.75"
23
+ "@team-agent/cli-darwin-arm64": "0.5.77",
24
+ "@team-agent/cli-darwin-x64": "0.5.77",
25
+ "@team-agent/cli-linux-x64": "0.5.77"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",