@team-agent/installer 0.5.49 → 0.5.51

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 (90) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +6 -4
  4. package/crates/team-agent/src/cli/emit.rs +91 -39
  5. package/crates/team-agent/src/cli/mod.rs +33 -15
  6. package/crates/team-agent/src/cli/named_address.rs +82 -53
  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 +103 -1308
  13. package/crates/team-agent/src/cli/spec.rs +2 -2
  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/leader_watch.rs +1 -1
  25. package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
  26. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
  27. package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
  28. package/crates/team-agent/src/cli/types.rs +5 -8
  29. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  30. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  31. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  32. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  33. package/crates/team-agent/src/db/message_store.rs +154 -44
  34. package/crates/team-agent/src/event_log.rs +73 -0
  35. package/crates/team-agent/src/leader/start.rs +28 -4
  36. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  37. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  38. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  39. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  41. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  43. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  44. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  45. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  46. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  48. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  49. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  51. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  52. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  53. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  54. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  55. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  56. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  57. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  58. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  59. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  61. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  62. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  63. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  64. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  65. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  66. package/crates/team-agent/src/mcp_server/mod.rs +2 -2
  67. package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
  68. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
  69. package/crates/team-agent/src/mcp_server/tools.rs +26 -15
  70. package/crates/team-agent/src/mcp_server/wire.rs +2 -18
  71. package/crates/team-agent/src/messaging/activity.rs +4 -2
  72. package/crates/team-agent/src/messaging/address.rs +86 -0
  73. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  74. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  75. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  76. package/crates/team-agent/src/messaging/mod.rs +11 -2
  77. package/crates/team-agent/src/messaging/persist.rs +309 -0
  78. package/crates/team-agent/src/messaging/results.rs +16 -24
  79. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  80. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  81. package/crates/team-agent/src/messaging/send.rs +133 -58
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +1 -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 +2 -1
  88. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  89. package/crates/team-agent/src/state/repository.rs +59 -16
  90. package/package.json +4 -4
@@ -0,0 +1,163 @@
1
+ use super::*;
2
+
3
+ pub(super) fn dirty_topology_refusal_value(
4
+ selected: &crate::state::selector::SelectedTeam,
5
+ requested_team: Option<&str>,
6
+ ) -> Option<Value> {
7
+ let issue_ids = crate::topology::restart_dirty_topology_issue_ids(&selected.state);
8
+ if issue_ids.is_empty() {
9
+ return None;
10
+ }
11
+ let session_name = selected
12
+ .state
13
+ .get("session_name")
14
+ .and_then(Value::as_str)
15
+ .unwrap_or_default()
16
+ .to_string();
17
+ let reason = issue_ids
18
+ .first()
19
+ .cloned()
20
+ .unwrap_or_else(|| "dirty_topology".to_string());
21
+ let repair_team = requested_team
22
+ .filter(|team| !team.is_empty())
23
+ .unwrap_or(selected.team_key.as_str());
24
+ Some(json!({
25
+ "ok": false,
26
+ "status": "refused_dirty_topology",
27
+ "reason": reason,
28
+ "session_name": session_name,
29
+ "error": "send refused: tmux endpoint/socket topology is inconsistent; run diagnose from the intended leader socket before sending",
30
+ "issues": issue_ids
31
+ .iter()
32
+ .map(|id| json!({"id": id}))
33
+ .collect::<Vec<_>>(),
34
+ "next_actions": [
35
+ "team-agent diagnose --json",
36
+ format!("team-agent claim-leader --team {repair_team} --confirm --json"),
37
+ format!("team-agent takeover --team {repair_team} --confirm --json")
38
+ ],
39
+ }))
40
+ }
41
+
42
+ pub(super) fn target_has_known_worker(state: &Value, target: &MessageTarget, sender: &str) -> bool {
43
+ let Some(agents) = state.get("agents").and_then(Value::as_object) else {
44
+ return false;
45
+ };
46
+ match target {
47
+ MessageTarget::Single(target) => agents.contains_key(target),
48
+ MessageTarget::Broadcast => agents.keys().any(|agent| agent != sender),
49
+ MessageTarget::Fanout(recipients) => recipients
50
+ .iter()
51
+ .any(|recipient| agents.contains_key(recipient)),
52
+ }
53
+ }
54
+
55
+ #[derive(Debug, Clone)]
56
+ pub(super) struct LoudEnsureResult {
57
+ previous_status: String,
58
+ start: crate::coordinator::StartReport,
59
+ }
60
+
61
+ pub(super) fn loud_ensure_coordinator(
62
+ selected: &crate::state::selector::SelectedTeam,
63
+ ) -> Result<Option<LoudEnsureResult>, CliError> {
64
+ if in_process_unit_test() {
65
+ return Ok(None);
66
+ }
67
+ let workspace = crate::coordinator::WorkspacePath::new(selected.run_workspace.clone());
68
+ let previous = crate::coordinator::coordinator_health(&workspace);
69
+ if previous.ok {
70
+ return Ok(None);
71
+ }
72
+ if previous.service_available
73
+ && matches!(
74
+ previous.binary_identity_relation,
75
+ crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
76
+ )
77
+ {
78
+ return Ok(None);
79
+ }
80
+ let previous_status = coordinator_health_status_wire(previous.status).to_string();
81
+ let start = crate::coordinator::start_coordinator_with_team(
82
+ &workspace,
83
+ Some(selected.team_key.as_str()),
84
+ )
85
+ .map_err(|error| CliError::Runtime(error.to_string()))?;
86
+ if !start.ok {
87
+ return Ok(Some(LoudEnsureResult {
88
+ previous_status,
89
+ start,
90
+ }));
91
+ }
92
+ if matches!(
93
+ start.status,
94
+ crate::coordinator::StartOutcome::Started
95
+ | crate::coordinator::StartOutcome::StartedAfterRotation
96
+ ) {
97
+ crate::event_log::EventLog::new(&selected.run_workspace)
98
+ .write(
99
+ "coordinator.ensure_restarted",
100
+ json!({
101
+ "coordinator_previous_status": previous_status,
102
+ "status": start.status,
103
+ "pid": start.pid.map(|pid| pid.get()),
104
+ "previous_pid": start.previous_pid.map(|pid| pid.get()),
105
+ "binary_path": start.binary_path,
106
+ "binary_version": start.binary_version,
107
+ "rotation_reason": start.rotation_reason,
108
+ }),
109
+ )
110
+ .map_err(|error| CliError::Runtime(error.to_string()))?;
111
+ return Ok(Some(LoudEnsureResult {
112
+ previous_status,
113
+ start,
114
+ }));
115
+ }
116
+ Ok(None)
117
+ }
118
+
119
+ #[cfg(test)]
120
+ pub(super) fn in_process_unit_test() -> bool {
121
+ true
122
+ }
123
+
124
+ #[cfg(not(test))]
125
+ pub(super) fn in_process_unit_test() -> bool {
126
+ false
127
+ }
128
+
129
+ pub(super) fn append_loud_ensure_fields(value: &mut Value, ensure: Option<&LoudEnsureResult>) {
130
+ let Some(ensure) = ensure else {
131
+ return;
132
+ };
133
+ if !ensure.start.ok {
134
+ return;
135
+ }
136
+ if let Some(obj) = value.as_object_mut() {
137
+ obj.insert("coordinator_auto_restarted".to_string(), json!(true));
138
+ obj.insert(
139
+ "coordinator_previous_status".to_string(),
140
+ json!(ensure.previous_status),
141
+ );
142
+ obj.insert(
143
+ "coordinator".to_string(),
144
+ coordinator_start_json(&ensure.start),
145
+ );
146
+ }
147
+ }
148
+
149
+ pub(super) fn coordinator_start_json(report: &crate::coordinator::StartReport) -> Value {
150
+ let summary = crate::lifecycle::CoordinatorStartSummary::from_start_report(report);
151
+ crate::lifecycle::coordinator_start_summary_value(&summary)
152
+ }
153
+
154
+ pub(super) fn coordinator_health_status_wire(
155
+ status: crate::coordinator::CoordinatorHealthStatus,
156
+ ) -> &'static str {
157
+ match status {
158
+ crate::coordinator::CoordinatorHealthStatus::Missing => "missing",
159
+ crate::coordinator::CoordinatorHealthStatus::InvalidPid => "invalid_pid",
160
+ crate::coordinator::CoordinatorHealthStatus::Running => "running",
161
+ crate::coordinator::CoordinatorHealthStatus::Stale => "stale",
162
+ }
163
+ }
@@ -0,0 +1,99 @@
1
+ use super::*;
2
+
3
+ pub(super) fn maybe_enqueue_offline_leader_mailbox(
4
+ sender_workspace: &Path,
5
+ to_name: &str,
6
+ content: &str,
7
+ sender: &str,
8
+ task_id: Option<&str>,
9
+ error: &crate::cli::named_address::NamedAddressError,
10
+ ) -> Result<Option<Value>, CliError> {
11
+ if error.kind != crate::cli::named_address::NamedAddressErrorKind::LeaderNotAttached {
12
+ return Ok(None);
13
+ }
14
+ let parsed = match crate::cli::named_address::parse_leader_target_workspace_and_team(
15
+ sender_workspace,
16
+ to_name,
17
+ ) {
18
+ Ok(Some(v)) => v,
19
+ Ok(None) => return Ok(None),
20
+ Err(_) => return Ok(None),
21
+ };
22
+ let (target_workspace, team_key) = parsed;
23
+ // Owner-scope refusal: sender workspace == target workspace. Keep
24
+ // the actionable attach hint (owner sees status/diagnose copy that
25
+ // points at `attach-leader`).
26
+ let sender_canonical =
27
+ std::fs::canonicalize(sender_workspace).unwrap_or_else(|_| sender_workspace.to_path_buf());
28
+ let target_canonical =
29
+ std::fs::canonicalize(&target_workspace).unwrap_or_else(|_| target_workspace.clone());
30
+ if sender_canonical == target_canonical {
31
+ return Ok(None);
32
+ }
33
+ // Verify the target team is actually alive on this host — mailbox
34
+ // is only for `team live + leader unattached`. Fail-closed otherwise
35
+ // so we never leave a message in a permanently-dead workspace's DB.
36
+ let state = match crate::state::persist::load_runtime_state(&target_workspace) {
37
+ Ok(s) => s,
38
+ Err(_) => return Ok(None),
39
+ };
40
+ let team_alive = target_team_is_alive_for_mailbox(&state, &team_key);
41
+ if !team_alive {
42
+ return Ok(None);
43
+ }
44
+ let event_log = crate::event_log::EventLog::new(&target_workspace);
45
+ let task = task_id.map(|s| crate::model::ids::TaskId::new(s.to_string()));
46
+ let outcome = messaging::enqueue_leader_mailbox_until_attach(
47
+ &target_workspace,
48
+ &team_key,
49
+ content,
50
+ task.as_ref(),
51
+ sender,
52
+ &event_log,
53
+ )
54
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
55
+ let message_id = outcome.message_id.clone().unwrap_or_else(|| "".to_string());
56
+ Ok(Some(json!({
57
+ "ok": true,
58
+ "status": "queued_until_leader_attach",
59
+ "message_status": "queued_until_leader_attach",
60
+ "channel": "leader_mailbox",
61
+ "delivered": false,
62
+ "to_name": to_name,
63
+ "target_workspace": target_workspace.display().to_string(),
64
+ "team_key": team_key,
65
+ "recipient": "leader",
66
+ "leader_attached": false,
67
+ "message_id": message_id,
68
+ })))
69
+ }
70
+
71
+ /// Positive-source liveness heuristic per offline-mailbox-toname-design.md §4:
72
+ /// - target workspace has state and the team key is present + not archived/down;
73
+ /// - AND at least one live tmux fact — a persisted `session_name` OR any
74
+ /// agent with a recorded pane on the recorded socket.
75
+ ///
76
+ /// We deliberately do NOT poll coordinator health here — enqueuing is
77
+ /// safe even when the coordinator is transiently down; attach-leader
78
+ /// itself replays via `requeue_blocked_leader_messages` regardless.
79
+ pub(super) fn target_team_is_alive_for_mailbox(state: &Value, team_key: &str) -> bool {
80
+ let team = state
81
+ .get("teams")
82
+ .and_then(|v| v.as_object())
83
+ .and_then(|teams| teams.get(team_key));
84
+ let Some(team) = team else {
85
+ return false;
86
+ };
87
+ let status = team
88
+ .get("status")
89
+ .and_then(|v| v.as_str())
90
+ .unwrap_or("alive");
91
+ if matches!(status, "archived" | "down" | "stopped") {
92
+ return false;
93
+ }
94
+ // A recorded session_name is enough — target's coordinator/attach
95
+ // path will re-verify tmux presence when the replay fires.
96
+ team.get("session_name")
97
+ .and_then(|v| v.as_str())
98
+ .is_some_and(|s| !s.is_empty())
99
+ }
@@ -0,0 +1,154 @@
1
+ use super::coordinator::{
2
+ append_loud_ensure_fields, dirty_topology_refusal_value, loud_ensure_coordinator,
3
+ target_has_known_worker,
4
+ };
5
+ use super::presentation::delivery_outcome_json;
6
+ use crate::cli::{CliError, SendArgs};
7
+ use crate::messaging::{
8
+ self, DeliveryOutcome, DeliveryStatus, MessageTarget, SendOptions, SendOrigin,
9
+ };
10
+ use crate::model::ids::{TaskId, TeamKey};
11
+ use serde_json::{json, Value};
12
+ use std::path::Path;
13
+
14
+ /// Translate SendArgs into the persisted-send options.
15
+ pub fn send_options_from_args(args: &SendArgs) -> SendOptions {
16
+ SendOptions {
17
+ origin: SendOrigin::Cli,
18
+ task_id: args.task.as_ref().map(|s| TaskId::new(s.clone())),
19
+ route_task_id: true,
20
+ sender: args.sender.clone(),
21
+ requires_ack: !args.no_ack,
22
+ confirm_human: args.confirm_human,
23
+ wait_visible: !args.no_wait,
24
+ timeout: args.timeout,
25
+ watch_result: args.watch_result,
26
+ team: args.team.as_ref().map(|s| TeamKey::new(s.clone())),
27
+ message_id: args.message_id.clone(),
28
+ ..SendOptions::default()
29
+ }
30
+ }
31
+
32
+ pub(super) fn persist_resolved_target(
33
+ args: &SendArgs,
34
+ resolved: &crate::cli::named_address::ResolvedNamedAddress,
35
+ target: &MessageTarget,
36
+ content: &str,
37
+ ) -> Result<Value, CliError> {
38
+ let selected = crate::state::selector::resolve_active_team(
39
+ &resolved.target_workspace,
40
+ resolved.team_key.as_deref(),
41
+ crate::state::selector::SelectorMode::RuntimeOnly,
42
+ )?;
43
+ if let Some(value) = dirty_topology_refusal_value(&selected, resolved.team_key.as_deref()) {
44
+ return Ok(value);
45
+ }
46
+ let mut opts = send_options_from_args(args);
47
+ opts.route_task_id = args.to_name.is_none() && args.to_leader.is_none();
48
+ opts.team = Some(TeamKey::new(selected.team_key.clone()));
49
+ let coordinator_ensure =
50
+ if target_has_known_worker(&selected.state, target, opts.sender.as_str()) {
51
+ loud_ensure_coordinator(&selected)?
52
+ } else {
53
+ None
54
+ };
55
+ let outcome = messaging::send_message(&selected.run_workspace, target, content, &opts)?;
56
+ let mut value = delivery_outcome_json(&outcome, target, content, &opts);
57
+ append_loud_ensure_fields(&mut value, coordinator_ensure.as_ref());
58
+ Ok(value)
59
+ }
60
+
61
+ pub(super) fn routing_ambiguous_value(
62
+ workspace: &Path,
63
+ args: &SendArgs,
64
+ target: &MessageTarget,
65
+ content: &str,
66
+ opts: &SendOptions,
67
+ ) -> Option<Value> {
68
+ if args.targets.is_some() || !content.is_empty() {
69
+ return None;
70
+ }
71
+ let MessageTarget::Single(name) = target else {
72
+ return None;
73
+ };
74
+ if name.is_empty() {
75
+ return None;
76
+ }
77
+ let state = crate::state::persist::load_runtime_state(workspace).ok()?;
78
+ let in_team = state
79
+ .get("agents")
80
+ .and_then(|v| v.as_object())
81
+ .is_some_and(|a| a.contains_key(name));
82
+ if in_team {
83
+ return None;
84
+ }
85
+ // aeab1c7 follow-up: `content` is no longer emitted anywhere from `send`
86
+ // responses (including this refusal). Replace with `content_length_bytes`
87
+ // to keep the size-sanity field consistent with the normal-send shape.
88
+ Some(json!({
89
+ "ok": false,
90
+ "status": "refused",
91
+ "target": null,
92
+ "agent_id": null,
93
+ "content_length_bytes": name.len(),
94
+ "sender": opts.sender,
95
+ "message_id": null,
96
+ "message_status": "refused",
97
+ "verification": null,
98
+ "stage": null,
99
+ "reason": "routing_ambiguous",
100
+ "channel": null,
101
+ }))
102
+ }
103
+
104
+ pub(super) fn selected_state_with_active_key(
105
+ selected: &crate::state::selector::SelectedTeam,
106
+ ) -> Value {
107
+ let mut state = selected.state.clone();
108
+ if let Some(obj) = state.as_object_mut() {
109
+ obj.insert(
110
+ "active_team_key".to_string(),
111
+ Value::String(selected.team_key.clone()),
112
+ );
113
+ }
114
+ state
115
+ }
116
+
117
+ pub(super) fn initial_delivery_allows_watch(status: DeliveryStatus) -> bool {
118
+ matches!(
119
+ status,
120
+ DeliveryStatus::Delivered | DeliveryStatus::AlreadyDelivered
121
+ )
122
+ }
123
+
124
+ pub(super) fn observe_initial_delivery_for_watch(
125
+ selected: &crate::state::selector::SelectedTeam,
126
+ target: &MessageTarget,
127
+ outcome: &DeliveryOutcome,
128
+ opts: &SendOptions,
129
+ ) -> Result<DeliveryOutcome, CliError> {
130
+ if !matches!(target, MessageTarget::Single(agent) if !agent.is_empty()) {
131
+ return Ok(outcome.clone());
132
+ }
133
+ if !matches!(outcome.status, DeliveryStatus::Queued) {
134
+ return Ok(outcome.clone());
135
+ }
136
+ let Some(message_id) = outcome.message_id.as_deref() else {
137
+ return Ok(outcome.clone());
138
+ };
139
+ let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
140
+ &selected.run_workspace,
141
+ opts.team.as_ref().map(TeamKey::as_str),
142
+ )
143
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
144
+ let event_log = crate::event_log::EventLog::new(&selected.run_workspace);
145
+ let state = selected_state_with_active_key(selected);
146
+ crate::messaging::deliver_persisted_message(
147
+ &selected.run_workspace,
148
+ &transport,
149
+ message_id,
150
+ &event_log,
151
+ &state,
152
+ )
153
+ .map_err(CliError::from)
154
+ }