@team-agent/installer 0.5.50 → 0.5.52

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 (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  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 +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  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/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -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 +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -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
+ }
@@ -0,0 +1,333 @@
1
+ use crate::cli::{CliError, CmdOutput, CmdResult, ExitCode};
2
+ use crate::messaging::{
3
+ DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessageTarget, SendOptions,
4
+ };
5
+ use serde_json::{json, Value};
6
+
7
+ pub(super) fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> Value {
8
+ let agent_id = match target {
9
+ MessageTarget::Single(agent) => agent.clone(),
10
+ MessageTarget::Broadcast => "*".to_string(),
11
+ MessageTarget::Fanout(recipients) => recipients
12
+ .first()
13
+ .cloned()
14
+ .unwrap_or_else(|| "-".to_string()),
15
+ };
16
+ json!({
17
+ "status": "registered",
18
+ "watcher_id": format!("watch-{agent_id}"),
19
+ "task_id": opts.task_id.as_ref().map(|t| t.as_str().to_string()),
20
+ "agent_id": agent_id,
21
+ "notice": "Team Agent will collect the result and notify the leader when this task reports completion."
22
+ })
23
+ }
24
+
25
+ /// 0.5.45 naming-addressing (design §3.5, RED-2/RED-3 positional):
26
+ /// after `messaging::send_message` refuses with `target_not_in_team`
27
+ /// for a Single non-special short id, attach scope-safe advisory
28
+ /// suggestions to the outbound JSON envelope. Candidate source =
29
+ /// selected team's projected `agents` map (never the raw workspace
30
+ /// `teams`) so sibling teams cannot leak. Zero DB write, zero inject
31
+ /// — the refusal exit code is unchanged.
32
+ pub(super) fn attach_positional_typo_suggestions(
33
+ value: &mut Value,
34
+ target: &MessageTarget,
35
+ selected_state: &Value,
36
+ ) {
37
+ use crate::model::name_similarity::{rank, Candidate};
38
+ let requested = match target {
39
+ MessageTarget::Single(id) if id != "*" && id != "leader" => id.clone(),
40
+ _ => return,
41
+ };
42
+ let Some(obj) = value.as_object_mut() else {
43
+ return;
44
+ };
45
+ if obj.get("reason").and_then(Value::as_str) != Some("target_not_in_team") {
46
+ return;
47
+ }
48
+ let team_key = selected_state
49
+ .get("active_team_key")
50
+ .or_else(|| selected_state.get("team_key"))
51
+ .and_then(Value::as_str)
52
+ .unwrap_or("");
53
+ let candidates: Vec<Candidate<String>> = selected_state
54
+ .get("agents")
55
+ .and_then(Value::as_object)
56
+ .map(|agents| {
57
+ agents
58
+ .keys()
59
+ .map(|agent_id| Candidate {
60
+ match_key: agent_id.clone(),
61
+ stable_key: agent_id.clone(),
62
+ payload: agent_id.clone(),
63
+ })
64
+ .collect()
65
+ })
66
+ .unwrap_or_default();
67
+ let ranked = rank(&requested, &candidates);
68
+ let candidate_values: Vec<Value> = ranked
69
+ .iter()
70
+ .map(|agent_id| {
71
+ json!({
72
+ "name": agent_id,
73
+ "team_key": team_key,
74
+ "agent_id": agent_id,
75
+ "advisory": true,
76
+ })
77
+ })
78
+ .collect();
79
+ obj.insert("requested_name".to_string(), json!(requested));
80
+ if let Some(best) = ranked.first() {
81
+ obj.insert("suggested_name".to_string(), json!(best));
82
+ }
83
+ obj.insert("candidates".to_string(), Value::Array(candidate_values));
84
+ }
85
+
86
+ pub(super) fn delivery_outcome_json(
87
+ outcome: &DeliveryOutcome,
88
+ target: &MessageTarget,
89
+ content: &str,
90
+ opts: &SendOptions,
91
+ ) -> Value {
92
+ // Pre-release 0.4.0 user directive: send result MUST NOT carry the
93
+ // message body — neither in human form (cli/emit.rs) NOR in --json.
94
+ // External consumers who need the message content read it via `inbox`,
95
+ // not from the send response. We surface `content_length_bytes` as a
96
+ // size sanity field so callers can verify the body size they intended
97
+ // to send arrived intact without exposing the body itself.
98
+ let target_wire = target_json(target);
99
+ json!({
100
+ "ok": outcome.ok,
101
+ "status": delivery_status_wire(outcome.status),
102
+ "delivery_status": api_delivery_status(outcome),
103
+ "delivered": delivery_proven(outcome.status),
104
+ "target": target_wire,
105
+ "agent_id": first_target(target),
106
+ "content_length_bytes": content.len(),
107
+ "sender": opts.sender,
108
+ "message_id": outcome.message_id,
109
+ "message_status": outcome.message_status.0,
110
+ "verification": outcome.verification,
111
+ "stage": outcome.stage.map(delivery_stage_wire),
112
+ "reason": outcome.reason.map(delivery_refusal_wire),
113
+ "channel": outcome.channel,
114
+ })
115
+ }
116
+
117
+ pub(super) fn api_delivery_status(outcome: &DeliveryOutcome) -> &'static str {
118
+ if delivery_proven(outcome.status) {
119
+ return "delivered";
120
+ }
121
+ if matches!(outcome.status, DeliveryStatus::Queued) && outcome.message_status.0 == "accepted" {
122
+ return "pending";
123
+ }
124
+ delivery_status_wire(outcome.status)
125
+ }
126
+
127
+ pub(super) fn delivery_proven(status: DeliveryStatus) -> bool {
128
+ matches!(
129
+ status,
130
+ DeliveryStatus::Delivered
131
+ | DeliveryStatus::AlreadyDelivered
132
+ | DeliveryStatus::BroadcastDelivered
133
+ | DeliveryStatus::FanoutDelivered
134
+ )
135
+ }
136
+
137
+ pub(super) fn add_send_reminder_if_ok(value: &mut Value) {
138
+ if value.get("ok").and_then(Value::as_bool) != Some(true) {
139
+ return;
140
+ }
141
+ let reminder = send_reminder_for_value(value);
142
+ if let Some(obj) = value.as_object_mut() {
143
+ obj.insert("reminder".to_string(), json!(reminder));
144
+ }
145
+ }
146
+
147
+ /// E6 (0.5.9 offline-mailbox-toname-design §§3.1/6.2/8, real-machine
148
+ /// escape evidence
149
+ /// `.team/artifacts/0.5.9-subscription-gate.md` +
150
+ /// `.team/evidence/0.5.9-subscription-gate-20260707T143241Z-4645/`):
151
+ /// when the `--to-name <ws>::<team>/leader` resolver refused with
152
+ /// `leader_not_attached`, decide whether the target team is still alive
153
+ /// (worker + coordinator running without a bound leader) and, if so,
154
+ /// enqueue the mailbox row so `attach-leader` replays it exactly once.
155
+ ///
156
+ /// Only queues for third-party senders (sender workspace ≠ target
157
+ /// workspace). Owner-scope refusals stay refused so status/diagnose can
158
+ /// keep pushing the operator toward `attach-leader`.
159
+ pub(super) fn cmd_send_result(value: Value, as_json: bool) -> CmdResult {
160
+ let exit = if value.get("ok").and_then(Value::as_bool) == Some(false) {
161
+ ExitCode::Error
162
+ } else {
163
+ ExitCode::Ok
164
+ };
165
+ if as_json {
166
+ CmdResult::from_json(value, true)
167
+ } else {
168
+ CmdResult {
169
+ output: CmdOutput::Human(send_human_output(&value)),
170
+ exit,
171
+ as_json: false,
172
+ }
173
+ }
174
+ }
175
+
176
+ pub(super) fn send_human_output(value: &Value) -> String {
177
+ let mut parts = vec![
178
+ send_human_field(value, "ok"),
179
+ format!("status: {}", send_human_status(value)),
180
+ send_human_field(value, "message_id"),
181
+ format!("target: {}", send_human_target(value)),
182
+ ];
183
+ for key in ["verification", "stage", "reason", "channel"] {
184
+ if !value.get(key).is_none_or(Value::is_null) {
185
+ parts.push(send_human_field(value, key));
186
+ }
187
+ }
188
+ // 0.5.45 naming-addressing (design §3.4/§3.5, RED-3 positional):
189
+ // when the refusal envelope carries a scope-safe suggestion,
190
+ // surface it verbatim in human output so users can copy the
191
+ // right short id. `requested_name` echoes the typo, `suggested_
192
+ // name` is the copyable canonical.
193
+ if let Some(requested) = value
194
+ .get("requested_name")
195
+ .and_then(Value::as_str)
196
+ .filter(|s| !s.is_empty())
197
+ {
198
+ parts.push(format!("requested_name: {requested}"));
199
+ }
200
+ if let Some(suggested) = value
201
+ .get("suggested_name")
202
+ .and_then(Value::as_str)
203
+ .filter(|s| !s.is_empty())
204
+ {
205
+ parts.push(format!(
206
+ "Did you mean `{suggested}`? suggested_name: {suggested}"
207
+ ));
208
+ }
209
+ parts.join(" ")
210
+ }
211
+
212
+ pub(super) fn send_human_field(value: &Value, key: &str) -> String {
213
+ let rendered = value
214
+ .get(key)
215
+ .map(send_human_value)
216
+ .unwrap_or_else(|| "None".to_string());
217
+ format!("{key}: {rendered}")
218
+ }
219
+
220
+ pub(super) fn send_human_target(value: &Value) -> String {
221
+ ["target", "agent_id", "pane_id", "to_name"]
222
+ .iter()
223
+ .find_map(|key| value.get(*key).filter(|v| !v.is_null()))
224
+ .map(send_human_value)
225
+ .unwrap_or_else(|| "None".to_string())
226
+ }
227
+
228
+ pub(super) fn send_human_status(value: &Value) -> String {
229
+ value
230
+ .get("status")
231
+ .map(send_human_value)
232
+ .unwrap_or_else(|| {
233
+ if value.get("ok").and_then(Value::as_bool) == Some(true) {
234
+ "delivered".to_string()
235
+ } else {
236
+ "failed".to_string()
237
+ }
238
+ })
239
+ }
240
+
241
+ pub(super) fn send_human_value(value: &Value) -> String {
242
+ let text = match value {
243
+ Value::Null => "None".to_string(),
244
+ Value::Bool(true) => "True".to_string(),
245
+ Value::Bool(false) => "False".to_string(),
246
+ Value::Number(n) => n.to_string(),
247
+ Value::String(s) => s.clone(),
248
+ Value::Array(_) | Value::Object(_) => {
249
+ serde_json::to_string(value).unwrap_or_else(|_| "None".to_string())
250
+ }
251
+ };
252
+ text.replace(['\r', '\n'], " ")
253
+ }
254
+
255
+ pub(super) fn send_reminder_for_value(value: &Value) -> &'static str {
256
+ let delivered = value.get("delivered").and_then(Value::as_bool);
257
+ let status = value.get("status").and_then(Value::as_str);
258
+ let delivery_status = value.get("delivery_status").and_then(Value::as_str);
259
+ if delivered == Some(false)
260
+ || matches!(status, Some("queued"))
261
+ || matches!(delivery_status, Some("pending"))
262
+ {
263
+ "Message queued; coordinator will notify when the worker receives it. Do not poll the worker terminal with capture-pane."
264
+ } else {
265
+ crate::cli::SEND_REMINDER
266
+ }
267
+ }
268
+
269
+ pub(super) fn target_json(target: &MessageTarget) -> Value {
270
+ match target {
271
+ MessageTarget::Single(agent) => json!(agent),
272
+ MessageTarget::Broadcast => json!("*"),
273
+ MessageTarget::Fanout(recipients) => json!(recipients),
274
+ }
275
+ }
276
+
277
+ pub(super) fn first_target(target: &MessageTarget) -> String {
278
+ match target {
279
+ MessageTarget::Single(agent) => agent.clone(),
280
+ MessageTarget::Broadcast => "*".to_string(),
281
+ MessageTarget::Fanout(recipients) => recipients.first().cloned().unwrap_or_default(),
282
+ }
283
+ }
284
+
285
+ pub(super) fn delivery_status_wire(status: DeliveryStatus) -> &'static str {
286
+ match status {
287
+ DeliveryStatus::Delivered => "delivered",
288
+ DeliveryStatus::Failed => "failed",
289
+ DeliveryStatus::Queued => "queued",
290
+ DeliveryStatus::Blocked => "blocked",
291
+ DeliveryStatus::Refused => "refused",
292
+ DeliveryStatus::Degraded => "degraded",
293
+ DeliveryStatus::RetryScheduled => "retry_scheduled",
294
+ DeliveryStatus::TrustAutoAnswerExhausted => "trust_auto_answer_exhausted",
295
+ DeliveryStatus::AlreadyDelivered => "already_delivered",
296
+ DeliveryStatus::FallbackLog => "fallback_log",
297
+ DeliveryStatus::BroadcastDelivered => "broadcast_delivered",
298
+ DeliveryStatus::BroadcastPartial => "broadcast_partial",
299
+ DeliveryStatus::FanoutDelivered => "fanout_delivered",
300
+ DeliveryStatus::FanoutPartial => "fanout_partial",
301
+ }
302
+ }
303
+
304
+ pub(super) fn delivery_refusal_wire(reason: DeliveryRefusal) -> &'static str {
305
+ match reason {
306
+ DeliveryRefusal::TargetNotInTeam => "target_not_in_team",
307
+ DeliveryRefusal::HumanConfirmationRequired => "human_confirmation_required",
308
+ DeliveryRefusal::MissingPermissions => "missing_permissions",
309
+ DeliveryRefusal::RecipientBusy => "recipient_busy",
310
+ DeliveryRefusal::UnknownRecipient => "unknown_recipient",
311
+ DeliveryRefusal::TmuxTargetMissing => "tmux_target_missing",
312
+ DeliveryRefusal::MessageAlreadyClaimed => "message_already_claimed",
313
+ DeliveryRefusal::LeaderNotAttached => "leader_not_attached",
314
+ DeliveryRefusal::CoordinatorUnavailable => "coordinator_unavailable",
315
+ DeliveryRefusal::NoCallerPane => "no_caller_pane",
316
+ DeliveryRefusal::TeamOwnerMismatch => "team_owner_mismatch",
317
+ DeliveryRefusal::Ambiguous => "ambiguous",
318
+ DeliveryRefusal::RecipientPaneInNonInputMode => "recipient_pane_in_non_input_mode",
319
+ DeliveryRefusal::SessionDrift => "session_drift",
320
+ DeliveryRefusal::Duplicate => "duplicate",
321
+ DeliveryRefusal::RoutingAmbiguous => "routing_ambiguous",
322
+ DeliveryRefusal::EmptyTargetList => "empty_target_list",
323
+ }
324
+ }
325
+
326
+ pub(super) fn delivery_stage_wire(stage: DeliveryStage) -> &'static str {
327
+ match stage {
328
+ DeliveryStage::TrustAutoAnswerDismissalWait => "trust_auto_answer_dismissal_wait",
329
+ DeliveryStage::Inject => "inject",
330
+ DeliveryStage::Submit => "submit",
331
+ DeliveryStage::VisibleCheck => "visible_check",
332
+ }
333
+ }