@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- 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
|
+
}
|