@team-agent/installer 0.3.0 → 0.3.2
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 +38 -7
- package/crates/team-agent/src/cli/emit.rs +182 -54
- package/crates/team-agent/src/cli/mod.rs +703 -35
- package/crates/team-agent/src/cli/status_port.rs +170 -44
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/health.rs +130 -0
- package/crates/team-agent/src/leader/lease.rs +23 -2
- package/crates/team-agent/src/leader/rediscover/tests.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +2 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +9 -6
- package/crates/team-agent/src/leader/tests/idle.rs +1 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +157 -0
- package/crates/team-agent/src/leader/types.rs +2 -0
- package/crates/team-agent/src/lifecycle/launch.rs +554 -65
- package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
- package/crates/team-agent/src/lifecycle/restart.rs +20 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +52 -0
- package/crates/team-agent/src/lifecycle/types.rs +25 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +28 -0
- package/crates/team-agent/src/mcp_server/wire.rs +81 -1
- package/crates/team-agent/src/messaging/delivery.rs +574 -12
- package/crates/team-agent/src/messaging/leader_receiver.rs +26 -37
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/results.rs +218 -49
- package/crates/team-agent/src/messaging/send.rs +15 -19
- package/crates/team-agent/src/provider/adapter.rs +95 -10
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/state/identity.rs +3 -0
- package/crates/team-agent/src/state/persist.rs +113 -1
- package/crates/team-agent/src/state/projection.rs +127 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +179 -0
- package/crates/team-agent/src/tmux_backend.rs +124 -12
- package/npm/install.mjs +29 -7
- package/package.json +4 -4
|
@@ -10,7 +10,7 @@ use crate::model::ids::{OwnerEpoch, TaskId};
|
|
|
10
10
|
use crate::transport::Transport;
|
|
11
11
|
|
|
12
12
|
use super::helpers::MessageStatusShadow;
|
|
13
|
-
use super::{DeliveryOutcome,
|
|
13
|
+
use super::{DeliveryOutcome, DeliveryStatus, MessagingError};
|
|
14
14
|
|
|
15
15
|
/// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
|
|
16
16
|
/// caller(send_message(to=leader) / report_result / request_human / idle reminder /
|
|
@@ -105,40 +105,6 @@ pub fn send_to_leader_receiver(
|
|
|
105
105
|
"result_id": result_id,
|
|
106
106
|
}),
|
|
107
107
|
)?;
|
|
108
|
-
// I-4: unbound leader pane → rebind_required (not the legacy diagnostic success).
|
|
109
|
-
// The row IS persisted (caller / rebind audit / future replay need the message_id),
|
|
110
|
-
// but marked `failed` so `deliver_pending_messages` does NOT pick it up — pane stays
|
|
111
|
-
// untouched, ok=false, channel=rebind_required. #231 auto-reclaim's
|
|
112
|
-
// requeue_after_claim_leader flips this row back to `accepted` after rebind, and
|
|
113
|
-
// deliver_pending replays it through the same pipeline (same message_id, exactly once).
|
|
114
|
-
let pane_attached = leader_pane_id(state)
|
|
115
|
-
.filter(|pane_id| leader_pane_is_live(workspace, pane_id))
|
|
116
|
-
.is_some();
|
|
117
|
-
if !pane_attached {
|
|
118
|
-
let _ = store.mark(&message_id, "failed", Some("leader_not_attached"));
|
|
119
|
-
event_log.write(
|
|
120
|
-
"leader_receiver.delivery_blocked",
|
|
121
|
-
serde_json::json!({
|
|
122
|
-
"message_id": message_id,
|
|
123
|
-
"sender": sender,
|
|
124
|
-
"leader_id": leader_id,
|
|
125
|
-
"owner_team_id": owner_team,
|
|
126
|
-
"reason": "leader_not_attached",
|
|
127
|
-
"channel": "rebind_required",
|
|
128
|
-
"action": "run team-agent claim-leader or team-agent takeover",
|
|
129
|
-
}),
|
|
130
|
-
)?;
|
|
131
|
-
return Ok(DeliveryOutcome {
|
|
132
|
-
ok: false,
|
|
133
|
-
status: DeliveryStatus::Blocked,
|
|
134
|
-
message_status: MessageStatusShadow("blocked".to_string()),
|
|
135
|
-
message_id: Some(message_id),
|
|
136
|
-
verification: None,
|
|
137
|
-
stage: None,
|
|
138
|
-
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
139
|
-
channel: Some("rebind_required".to_string()),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
108
|
event_log.write(
|
|
143
109
|
"leader_receiver.queued",
|
|
144
110
|
serde_json::json!({
|
|
@@ -221,6 +187,15 @@ pub fn claim_leader_receiver(
|
|
|
221
187
|
copy_candidate_field(receiver, candidate, "pane_id");
|
|
222
188
|
copy_candidate_field(receiver, candidate, "provider");
|
|
223
189
|
copy_candidate_field(receiver, candidate, "leader_session_uuid");
|
|
190
|
+
if let Some(socket) = candidate
|
|
191
|
+
.get("tmux_socket")
|
|
192
|
+
.and_then(Value::as_str)
|
|
193
|
+
.filter(|socket| std::path::Path::new(socket).is_absolute())
|
|
194
|
+
.map(str::to_string)
|
|
195
|
+
.or_else(crate::tmux_backend::socket_name_from_tmux_env)
|
|
196
|
+
{
|
|
197
|
+
receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
198
|
+
}
|
|
224
199
|
}
|
|
225
200
|
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
226
201
|
event_log.write(
|
|
@@ -310,10 +285,17 @@ fn leader_session_uuid(state: &Value) -> Option<&str> {
|
|
|
310
285
|
|
|
311
286
|
pub(crate) fn leader_pane_bound_but_not_live(workspace: &Path, state: &Value) -> bool {
|
|
312
287
|
leader_pane_id(state)
|
|
313
|
-
.is_some_and(|pane_id| !leader_pane_is_live(workspace, pane_id))
|
|
288
|
+
.is_some_and(|pane_id| !leader_pane_is_live(workspace, state, pane_id))
|
|
314
289
|
}
|
|
315
290
|
|
|
316
|
-
fn leader_pane_is_live(workspace: &Path, pane_id: &str) -> bool {
|
|
291
|
+
fn leader_pane_is_live(workspace: &Path, state: &Value, pane_id: &str) -> bool {
|
|
292
|
+
if let Some(socket) = leader_tmux_socket(state) {
|
|
293
|
+
return crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket)
|
|
294
|
+
.list_targets()
|
|
295
|
+
.unwrap_or_default()
|
|
296
|
+
.iter()
|
|
297
|
+
.any(|target| target.pane_id.as_str() == pane_id);
|
|
298
|
+
}
|
|
317
299
|
let mut targets = crate::tmux_backend::TmuxBackend::for_workspace(workspace)
|
|
318
300
|
.list_targets()
|
|
319
301
|
.unwrap_or_default();
|
|
@@ -329,6 +311,13 @@ fn leader_pane_id(state: &Value) -> Option<&str> {
|
|
|
329
311
|
leader_record_field(state, "pane_id").and_then(Value::as_str)
|
|
330
312
|
}
|
|
331
313
|
|
|
314
|
+
fn leader_tmux_socket(state: &Value) -> Option<&str> {
|
|
315
|
+
leader_record_field(state, "tmux_socket")
|
|
316
|
+
.and_then(Value::as_str)
|
|
317
|
+
.filter(|socket| !socket.is_empty())
|
|
318
|
+
.filter(|socket| std::path::Path::new(socket).is_absolute())
|
|
319
|
+
}
|
|
320
|
+
|
|
332
321
|
fn copy_candidate_field(
|
|
333
322
|
out: &mut serde_json::Map<String, Value>,
|
|
334
323
|
candidate: &Value,
|
|
@@ -86,7 +86,7 @@ pub use leader_receiver::{
|
|
|
86
86
|
claim_leader_receiver, mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
87
87
|
};
|
|
88
88
|
pub use peers::allow_peer_talk;
|
|
89
|
-
pub use results::{collect, collect_results_and_notify_watchers, report_result};
|
|
89
|
+
pub use results::{collect, collect_for_team, collect_results_and_notify_watchers, report_result};
|
|
90
90
|
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel, stuck_list};
|
|
91
91
|
pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
|
|
92
92
|
pub use send::{apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions};
|
|
@@ -10,6 +10,7 @@ use crate::message_store::MessageStore;
|
|
|
10
10
|
use super::helpers::{next_result_id, required_str, validate_result_envelope};
|
|
11
11
|
use super::types::SEND_RETRY_MAX_ATTEMPTS;
|
|
12
12
|
use crate::model::ids::TaskId;
|
|
13
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
13
14
|
use super::watchers::retry_result_deliveries;
|
|
14
15
|
use super::MessagingError;
|
|
15
16
|
|
|
@@ -19,26 +20,65 @@ pub fn collect(
|
|
|
19
20
|
workspace: &Path,
|
|
20
21
|
result_file: Option<&Path>,
|
|
21
22
|
ensure_coordinator: bool,
|
|
23
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
24
|
+
collect_scoped(workspace, result_file, ensure_coordinator, None)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn collect_for_team(
|
|
28
|
+
workspace: &Path,
|
|
29
|
+
result_file: Option<&Path>,
|
|
30
|
+
ensure_coordinator: bool,
|
|
31
|
+
owner_team_id: Option<&str>,
|
|
32
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
33
|
+
collect_scoped(workspace, result_file, ensure_coordinator, owner_team_id)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn collect_scoped(
|
|
37
|
+
workspace: &Path,
|
|
38
|
+
result_file: Option<&Path>,
|
|
39
|
+
ensure_coordinator: bool,
|
|
40
|
+
owner_team_id: Option<&str>,
|
|
22
41
|
) -> Result<serde_json::Value, MessagingError> {
|
|
23
42
|
let _ = ensure_coordinator;
|
|
24
43
|
let paths = collect_paths(workspace)?;
|
|
25
|
-
let
|
|
44
|
+
let log = EventLog::new(&paths.run_workspace);
|
|
45
|
+
let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
|
|
46
|
+
Some(team) => Some(resolve_owner_team_for_read(&paths.run_workspace, team, Some(&log))?),
|
|
47
|
+
None => None,
|
|
48
|
+
};
|
|
49
|
+
let owner_team_id = resolved_owner_team_id.as_deref();
|
|
50
|
+
let mut state = match owner_team_id {
|
|
51
|
+
Some(team) => crate::state::projection::select_runtime_state(&paths.run_workspace, Some(team))?,
|
|
52
|
+
None => crate::state::persist::load_runtime_state(&paths.run_workspace)?,
|
|
53
|
+
};
|
|
54
|
+
let spec_workspace = owner_team_id
|
|
55
|
+
.and_then(|_| state_spec_workspace_from_value(&state))
|
|
56
|
+
.unwrap_or_else(|| paths.spec_workspace.clone());
|
|
57
|
+
let spec_path = spec_workspace.join("team.spec.yaml");
|
|
26
58
|
if !spec_path.exists() {
|
|
27
59
|
return Err(MessagingError::Validation(format!("Cannot read {}", spec_path.display())));
|
|
28
60
|
}
|
|
29
61
|
let store = MessageStore::open(&paths.run_workspace)?;
|
|
30
62
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
31
63
|
if let Some(path) = result_file {
|
|
32
|
-
ingest_result_file(&conn, path)?;
|
|
64
|
+
ingest_result_file(&conn, path, owner_team_id)?;
|
|
33
65
|
}
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
let sql = match owner_team_id {
|
|
67
|
+
Some(_) => {
|
|
68
|
+
"select result_id, task_id, agent_id, envelope, status, created_at
|
|
69
|
+
from results
|
|
70
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
71
|
+
order by created_at, result_id"
|
|
72
|
+
}
|
|
73
|
+
None => {
|
|
74
|
+
"select result_id, task_id, agent_id, envelope, status, created_at
|
|
75
|
+
from results
|
|
76
|
+
where status not in ('collected', 'invalid')
|
|
77
|
+
order by created_at, result_id"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
let mut stmt = conn.prepare(sql)?;
|
|
81
|
+
let row_mapper = |row: &rusqlite::Row<'_>| {
|
|
42
82
|
Ok(StoredResult {
|
|
43
83
|
result_id: row.get(0)?,
|
|
44
84
|
task_id: row.get(1)?,
|
|
@@ -47,16 +87,18 @@ pub fn collect(
|
|
|
47
87
|
status: row.get(4)?,
|
|
48
88
|
created_at: row.get(5)?,
|
|
49
89
|
})
|
|
50
|
-
}
|
|
90
|
+
};
|
|
91
|
+
let rows = match owner_team_id {
|
|
92
|
+
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
93
|
+
None => stmt.query_map([], row_mapper),
|
|
94
|
+
}?
|
|
51
95
|
.collect::<Result<Vec<_>, _>>()?;
|
|
52
96
|
drop(stmt);
|
|
53
97
|
|
|
54
|
-
let mut state = crate::state::persist::load_runtime_state(&paths.run_workspace)?;
|
|
55
98
|
let mut collected = Vec::new();
|
|
56
99
|
let mut collected_results = Vec::new();
|
|
57
100
|
let mut invalid_results = Vec::new();
|
|
58
101
|
let mut state_dirty = false;
|
|
59
|
-
let log = EventLog::new(&paths.run_workspace);
|
|
60
102
|
for row in rows {
|
|
61
103
|
let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
|
|
62
104
|
Ok(envelope) => envelope,
|
|
@@ -83,7 +125,7 @@ pub fn collect(
|
|
|
83
125
|
}
|
|
84
126
|
let scope = if task_exists(&state, &row.task_id) {
|
|
85
127
|
"task"
|
|
86
|
-
} else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id)? {
|
|
128
|
+
} else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id, owner_team_id)? {
|
|
87
129
|
"message"
|
|
88
130
|
} else {
|
|
89
131
|
record_invalid_result(
|
|
@@ -95,10 +137,20 @@ pub fn collect(
|
|
|
95
137
|
)?;
|
|
96
138
|
continue;
|
|
97
139
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
match owner_team_id {
|
|
141
|
+
Some(team) => {
|
|
142
|
+
conn.execute(
|
|
143
|
+
"update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2",
|
|
144
|
+
params![row.result_id.as_str(), team],
|
|
145
|
+
)?;
|
|
146
|
+
}
|
|
147
|
+
None => {
|
|
148
|
+
conn.execute(
|
|
149
|
+
"update results set status = 'collected' where result_id = ?1",
|
|
150
|
+
params![row.result_id.as_str()],
|
|
151
|
+
)?;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
102
154
|
if scope == "task" {
|
|
103
155
|
mark_task_done(&mut state, &row.task_id, &row.result_id);
|
|
104
156
|
state_dirty = true;
|
|
@@ -129,9 +181,13 @@ pub fn collect(
|
|
|
129
181
|
collected_results.push(summary);
|
|
130
182
|
}
|
|
131
183
|
if state_dirty {
|
|
132
|
-
|
|
184
|
+
if owner_team_id.is_some() {
|
|
185
|
+
crate::state::projection::save_team_scoped_state(&paths.run_workspace, &state)?;
|
|
186
|
+
} else {
|
|
187
|
+
crate::state::persist::save_runtime_state(&paths.run_workspace, &state)?;
|
|
188
|
+
}
|
|
133
189
|
}
|
|
134
|
-
let counts = result_counts(&conn)?;
|
|
190
|
+
let counts = result_counts(&conn, owner_team_id)?;
|
|
135
191
|
Ok(serde_json::json!({
|
|
136
192
|
"ok": invalid_results.is_empty(),
|
|
137
193
|
"collected": collected,
|
|
@@ -139,7 +195,7 @@ pub fn collect(
|
|
|
139
195
|
"delivered_messages": [],
|
|
140
196
|
"invalid_results": invalid_results,
|
|
141
197
|
"results": counts,
|
|
142
|
-
"state_file":
|
|
198
|
+
"state_file": spec_workspace.join("team_state.md").to_string_lossy().to_string(),
|
|
143
199
|
"coordinator": {
|
|
144
200
|
"ok": false,
|
|
145
201
|
"status": "not_required",
|
|
@@ -147,12 +203,48 @@ pub fn collect(
|
|
|
147
203
|
}))
|
|
148
204
|
}
|
|
149
205
|
|
|
206
|
+
fn resolve_owner_team_for_read(
|
|
207
|
+
workspace: &Path,
|
|
208
|
+
requested: &str,
|
|
209
|
+
event_log: Option<&EventLog>,
|
|
210
|
+
) -> Result<String, MessagingError> {
|
|
211
|
+
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
212
|
+
match crate::state::projection::resolve_owner_team_id(&state, requested) {
|
|
213
|
+
OwnerTeamResolution::Canonical(canonical) => Ok(canonical),
|
|
214
|
+
OwnerTeamResolution::LegacyAlias { requested, canonical } => {
|
|
215
|
+
crate::messaging::delivery::normalize_owner_team_id_rows(
|
|
216
|
+
workspace,
|
|
217
|
+
&requested,
|
|
218
|
+
&canonical,
|
|
219
|
+
None,
|
|
220
|
+
event_log,
|
|
221
|
+
)?;
|
|
222
|
+
Ok(canonical)
|
|
223
|
+
}
|
|
224
|
+
OwnerTeamResolution::Unresolved { requested } => {
|
|
225
|
+
Err(MessagingError::Routing(format!("owner_team_unresolved: {requested}")))
|
|
226
|
+
}
|
|
227
|
+
OwnerTeamResolution::Ambiguous { requested, matches } => {
|
|
228
|
+
Err(MessagingError::Routing(format!(
|
|
229
|
+
"owner_team_ambiguous: {requested} matches {}",
|
|
230
|
+
matches.join(",")
|
|
231
|
+
)))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
150
236
|
struct CollectPaths {
|
|
151
237
|
run_workspace: PathBuf,
|
|
152
238
|
spec_workspace: PathBuf,
|
|
153
239
|
}
|
|
154
240
|
|
|
155
241
|
fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
|
|
242
|
+
if collect_input_has_no_local_team_context(workspace) {
|
|
243
|
+
return Ok(CollectPaths {
|
|
244
|
+
run_workspace: workspace.to_path_buf(),
|
|
245
|
+
spec_workspace: workspace.to_path_buf(),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
156
248
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
157
249
|
.map_err(|e| MessagingError::Routing(e.to_string()))?;
|
|
158
250
|
let spec_workspace = if workspace.join("team.spec.yaml").exists() {
|
|
@@ -168,8 +260,24 @@ fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
|
|
|
168
260
|
})
|
|
169
261
|
}
|
|
170
262
|
|
|
263
|
+
fn collect_input_has_no_local_team_context(workspace: &Path) -> bool {
|
|
264
|
+
!workspace.join("team.spec.yaml").exists()
|
|
265
|
+
&& !workspace.join(".team").exists()
|
|
266
|
+
&& !crate::state::persist::runtime_state_path(workspace).exists()
|
|
267
|
+
&& workspace.file_name().and_then(|s| s.to_str()) != Some(".team")
|
|
268
|
+
&& workspace
|
|
269
|
+
.parent()
|
|
270
|
+
.and_then(|p| p.file_name())
|
|
271
|
+
.and_then(|s| s.to_str())
|
|
272
|
+
!= Some(".team")
|
|
273
|
+
}
|
|
274
|
+
|
|
171
275
|
fn state_spec_workspace(run_workspace: &Path) -> Option<PathBuf> {
|
|
172
276
|
let state = crate::state::persist::load_runtime_state(run_workspace).ok()?;
|
|
277
|
+
state_spec_workspace_from_value(&state)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn state_spec_workspace_from_value(state: &serde_json::Value) -> Option<PathBuf> {
|
|
173
281
|
if let Some(spec_path) = state.get("spec_path").and_then(serde_json::Value::as_str) {
|
|
174
282
|
return PathBuf::from(spec_path).parent().map(Path::to_path_buf);
|
|
175
283
|
}
|
|
@@ -200,7 +308,11 @@ fn record_invalid_result(
|
|
|
200
308
|
Ok(())
|
|
201
309
|
}
|
|
202
310
|
|
|
203
|
-
fn ingest_result_file(
|
|
311
|
+
fn ingest_result_file(
|
|
312
|
+
conn: &rusqlite::Connection,
|
|
313
|
+
path: &Path,
|
|
314
|
+
owner_team_id: Option<&str>,
|
|
315
|
+
) -> Result<(), MessagingError> {
|
|
204
316
|
let raw = std::fs::read_to_string(path)?;
|
|
205
317
|
let mut envelope: serde_json::Value = serde_json::from_str(&raw)?;
|
|
206
318
|
validate_result_envelope(&envelope)?;
|
|
@@ -226,7 +338,7 @@ fn ingest_result_file(conn: &rusqlite::Connection, path: &Path) -> Result<(), Me
|
|
|
226
338
|
agent_id,
|
|
227
339
|
&envelope.to_string(),
|
|
228
340
|
status,
|
|
229
|
-
|
|
341
|
+
owner_team_id,
|
|
230
342
|
)?;
|
|
231
343
|
Ok(())
|
|
232
344
|
}
|
|
@@ -273,39 +385,55 @@ fn is_message_scoped_result(
|
|
|
273
385
|
conn: &rusqlite::Connection,
|
|
274
386
|
task_id: &str,
|
|
275
387
|
agent_id: &str,
|
|
388
|
+
owner_team_id: Option<&str>,
|
|
276
389
|
) -> Result<bool, MessagingError> {
|
|
277
390
|
if !task_id.starts_with("msg_") {
|
|
278
391
|
return Ok(false);
|
|
279
392
|
}
|
|
280
|
-
let count: i64 =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
393
|
+
let count: i64 = match owner_team_id {
|
|
394
|
+
Some(team) => conn.query_row(
|
|
395
|
+
"select count(*) from messages where message_id = ?1 and recipient = ?2 and owner_team_id = ?3",
|
|
396
|
+
params![task_id, agent_id, team],
|
|
397
|
+
|row| row.get(0),
|
|
398
|
+
)?,
|
|
399
|
+
None => conn.query_row(
|
|
400
|
+
"select count(*) from messages where message_id = ?1 and recipient = ?2",
|
|
401
|
+
params![task_id, agent_id],
|
|
402
|
+
|row| row.get(0),
|
|
403
|
+
)?,
|
|
404
|
+
};
|
|
285
405
|
Ok(count > 0)
|
|
286
406
|
}
|
|
287
407
|
|
|
288
|
-
fn result_counts(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)?;
|
|
295
|
-
let invalid: i64 = conn.query_row(
|
|
296
|
-
"select count(*) from results where status = 'invalid'",
|
|
297
|
-
[],
|
|
298
|
-
|row| row.get(0),
|
|
299
|
-
)?;
|
|
408
|
+
fn result_counts(
|
|
409
|
+
conn: &rusqlite::Connection,
|
|
410
|
+
owner_team_id: Option<&str>,
|
|
411
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
412
|
+
let total: i64 = count_results(conn, owner_team_id, None)?;
|
|
413
|
+
let collected: i64 = count_results(conn, owner_team_id, Some("collected"))?;
|
|
414
|
+
let invalid: i64 = count_results(conn, owner_team_id, Some("invalid"))?;
|
|
300
415
|
let uncollected = total - collected - invalid;
|
|
301
416
|
let mut by_status = serde_json::Map::new();
|
|
302
|
-
let
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
417
|
+
let sql = match owner_team_id {
|
|
418
|
+
Some(_) => {
|
|
419
|
+
"select status, count(*) from results
|
|
420
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
421
|
+
group by status
|
|
422
|
+
order by status"
|
|
423
|
+
}
|
|
424
|
+
None => {
|
|
425
|
+
"select status, count(*) from results
|
|
426
|
+
where status not in ('collected', 'invalid')
|
|
427
|
+
group by status
|
|
428
|
+
order by status"
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
let mut stmt = conn.prepare(sql)?;
|
|
432
|
+
let row_mapper = |row: &rusqlite::Row<'_>| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?));
|
|
433
|
+
let rows = match owner_team_id {
|
|
434
|
+
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
435
|
+
None => stmt.query_map([], row_mapper),
|
|
436
|
+
}?;
|
|
309
437
|
for row in rows {
|
|
310
438
|
let (status, count) = row?;
|
|
311
439
|
by_status.insert(status, serde_json::Value::Number(count.into()));
|
|
@@ -319,6 +447,31 @@ fn result_counts(conn: &rusqlite::Connection) -> Result<serde_json::Value, Messa
|
|
|
319
447
|
}))
|
|
320
448
|
}
|
|
321
449
|
|
|
450
|
+
fn count_results(
|
|
451
|
+
conn: &rusqlite::Connection,
|
|
452
|
+
owner_team_id: Option<&str>,
|
|
453
|
+
status: Option<&str>,
|
|
454
|
+
) -> Result<i64, MessagingError> {
|
|
455
|
+
match (owner_team_id, status) {
|
|
456
|
+
(Some(team), Some(status)) => Ok(conn.query_row(
|
|
457
|
+
"select count(*) from results where owner_team_id = ?1 and status = ?2",
|
|
458
|
+
params![team, status],
|
|
459
|
+
|row| row.get(0),
|
|
460
|
+
)?),
|
|
461
|
+
(Some(team), None) => Ok(conn.query_row(
|
|
462
|
+
"select count(*) from results where owner_team_id = ?1",
|
|
463
|
+
params![team],
|
|
464
|
+
|row| row.get(0),
|
|
465
|
+
)?),
|
|
466
|
+
(None, Some(status)) => Ok(conn.query_row(
|
|
467
|
+
"select count(*) from results where status = ?1",
|
|
468
|
+
params![status],
|
|
469
|
+
|row| row.get(0),
|
|
470
|
+
)?),
|
|
471
|
+
(None, None) => Ok(conn.query_row("select count(*) from results", [], |row| row.get(0))?),
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
322
475
|
/// `report_result` (`results.py:191`):worker 报结果 —— 校验 envelope、存 result、ack 任务消息、
|
|
323
476
|
/// **排队** send 事件通知 leader、推进 orchestrator (软依赖,失败仅记 `orchestrator.advance_skipped`)。
|
|
324
477
|
/// MCP `report_result` 工具调。
|
|
@@ -394,7 +547,7 @@ pub fn report_result(
|
|
|
394
547
|
let content = format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
|
|
395
548
|
let state = crate::state::persist::load_runtime_state(workspace).unwrap_or(serde_json::json!({}));
|
|
396
549
|
let event_log = EventLog::new(workspace);
|
|
397
|
-
let outcome = super::leader_receiver::send_to_leader_receiver(
|
|
550
|
+
let mut outcome = super::leader_receiver::send_to_leader_receiver(
|
|
398
551
|
workspace,
|
|
399
552
|
&state,
|
|
400
553
|
"leader",
|
|
@@ -405,10 +558,26 @@ pub fn report_result(
|
|
|
405
558
|
Some(&result_id),
|
|
406
559
|
&event_log,
|
|
407
560
|
)?;
|
|
561
|
+
if matches!(outcome.status, crate::messaging::DeliveryStatus::Queued) {
|
|
562
|
+
if let Some(message_id) = outcome.message_id.clone() {
|
|
563
|
+
let store = MessageStore::open(workspace)?;
|
|
564
|
+
let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
565
|
+
outcome = super::delivery::deliver_pending_message(
|
|
566
|
+
workspace,
|
|
567
|
+
&store,
|
|
568
|
+
&transport,
|
|
569
|
+
&message_id,
|
|
570
|
+
&event_log,
|
|
571
|
+
&state,
|
|
572
|
+
)?;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
408
575
|
let leader_notified = outcome.ok;
|
|
409
576
|
let notification_status_wire = if outcome.ok {
|
|
410
577
|
"delivered"
|
|
411
|
-
} else if
|
|
578
|
+
} else if outcome.channel.as_deref() == Some("rebind_required")
|
|
579
|
+
|| matches!(outcome.status, crate::messaging::DeliveryStatus::Blocked)
|
|
580
|
+
{
|
|
412
581
|
"rebind_required"
|
|
413
582
|
} else {
|
|
414
583
|
"refused"
|
|
@@ -8,7 +8,7 @@ use crate::model::enums::PaneLiveness;
|
|
|
8
8
|
use crate::transport::{PaneId, Transport};
|
|
9
9
|
|
|
10
10
|
use super::helpers::{status_wire, MessageStatusShadow};
|
|
11
|
-
use super::leader_receiver::
|
|
11
|
+
use super::leader_receiver::send_to_leader_receiver;
|
|
12
12
|
use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
|
|
13
13
|
|
|
14
14
|
/// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
|
|
@@ -86,25 +86,9 @@ pub fn send_message(
|
|
|
86
86
|
.map(|team| crate::state::projection::project_top_level_view(&raw_state, team.as_str()))
|
|
87
87
|
.unwrap_or_else(|| raw_state.clone());
|
|
88
88
|
backfill_leader_binding_for_delivery_view(&mut state, &raw_state);
|
|
89
|
-
let target_is_leader = matches!(target, MessageTarget::Single(target) if target == "leader");
|
|
90
|
-
if target_is_leader
|
|
91
|
-
&& sender_is_leader(&state, &opts.sender)
|
|
92
|
-
&& leader_pane_bound_but_not_live(workspace, &state)
|
|
93
|
-
{
|
|
94
|
-
event_log.write(
|
|
95
|
-
"leader_receiver.delivery_blocked",
|
|
96
|
-
serde_json::json!({
|
|
97
|
-
"sender": opts.sender,
|
|
98
|
-
"reason": "leader_not_attached",
|
|
99
|
-
"channel": "rebind_required",
|
|
100
|
-
"action": "run team-agent claim-leader or team-agent takeover",
|
|
101
|
-
}),
|
|
102
|
-
)?;
|
|
103
|
-
return Ok(rebind_required_outcome(None));
|
|
104
|
-
}
|
|
105
89
|
let recipient = match target {
|
|
106
90
|
MessageTarget::Single(target) if target == "leader" => {
|
|
107
|
-
|
|
91
|
+
let outcome = send_to_leader_receiver(
|
|
108
92
|
workspace,
|
|
109
93
|
&state,
|
|
110
94
|
"leader",
|
|
@@ -114,7 +98,19 @@ pub fn send_message(
|
|
|
114
98
|
opts.requires_ack,
|
|
115
99
|
None,
|
|
116
100
|
&event_log,
|
|
117
|
-
)
|
|
101
|
+
)?;
|
|
102
|
+
if matches!(outcome.status, DeliveryStatus::Queued) && owner_pane_is_dead(&state) {
|
|
103
|
+
if let Some(message_id) = outcome.message_id.clone() {
|
|
104
|
+
let team_key = owner_gate_hint_team_key(&state);
|
|
105
|
+
if !explicit_claim_applied(workspace, &team_key, "") {
|
|
106
|
+
return Ok(rebind_required_outcome_with_verification(
|
|
107
|
+
Some(message_id),
|
|
108
|
+
format!("team-agent claim-leader --team {team_key}"),
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return Ok(outcome);
|
|
118
114
|
}
|
|
119
115
|
MessageTarget::Single(target) if target.is_empty() => {
|
|
120
116
|
return Ok(refused_outcome(DeliveryRefusal::UnknownRecipient));
|