@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -86,18 +86,21 @@ 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::{
|
|
89
|
+
pub use results::{
|
|
90
|
+
collect, collect_for_team, collect_results_and_notify_watchers, report_result,
|
|
91
|
+
report_result_for_owner_team,
|
|
92
|
+
};
|
|
90
93
|
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel, stuck_list};
|
|
91
94
|
pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
|
|
92
95
|
pub use send::{apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions};
|
|
93
96
|
pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
|
|
94
97
|
pub use types::{
|
|
95
98
|
ActivityStatus, AgentActivity, AlertSnapshot, AlertSuppression, AlertType, CheckEvidence,
|
|
96
|
-
CheckKind, CheckStatus, DeliveryOutcome, DeliveryRefusal, DeliveryStage,
|
|
97
|
-
IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
98
|
-
ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
99
|
-
WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS,
|
|
100
|
-
TRUST_RETRY_MAX_ATTEMPTS,
|
|
99
|
+
CheckKind, CheckStatus, ContractSuiteCheck, DeliveryOutcome, DeliveryRefusal, DeliveryStage,
|
|
100
|
+
DeliveryStatus, IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
101
|
+
ProviderSdkCalls, ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
102
|
+
TrustRetryPayload, WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS,
|
|
103
|
+
TRUST_RETRY_BACKOFF_SECONDS, TRUST_RETRY_MAX_ATTEMPTS,
|
|
101
104
|
};
|
|
102
105
|
pub use watchers::{
|
|
103
106
|
delivered_result_message, format_result_watcher_notification, notify_result_watchers,
|
|
@@ -6,10 +6,12 @@ use rusqlite::params;
|
|
|
6
6
|
|
|
7
7
|
use crate::event_log::EventLog;
|
|
8
8
|
use crate::message_store::MessageStore;
|
|
9
|
+
use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
|
|
9
10
|
|
|
10
11
|
use super::helpers::{next_result_id, required_str, validate_result_envelope};
|
|
11
12
|
use super::types::SEND_RETRY_MAX_ATTEMPTS;
|
|
12
13
|
use crate::model::ids::TaskId;
|
|
14
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
13
15
|
use super::watchers::retry_result_deliveries;
|
|
14
16
|
use super::MessagingError;
|
|
15
17
|
|
|
@@ -19,26 +21,65 @@ pub fn collect(
|
|
|
19
21
|
workspace: &Path,
|
|
20
22
|
result_file: Option<&Path>,
|
|
21
23
|
ensure_coordinator: bool,
|
|
24
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
25
|
+
collect_scoped(workspace, result_file, ensure_coordinator, None)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn collect_for_team(
|
|
29
|
+
workspace: &Path,
|
|
30
|
+
result_file: Option<&Path>,
|
|
31
|
+
ensure_coordinator: bool,
|
|
32
|
+
owner_team_id: Option<&str>,
|
|
33
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
34
|
+
collect_scoped(workspace, result_file, ensure_coordinator, owner_team_id)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn collect_scoped(
|
|
38
|
+
workspace: &Path,
|
|
39
|
+
result_file: Option<&Path>,
|
|
40
|
+
ensure_coordinator: bool,
|
|
41
|
+
owner_team_id: Option<&str>,
|
|
22
42
|
) -> Result<serde_json::Value, MessagingError> {
|
|
23
43
|
let _ = ensure_coordinator;
|
|
24
44
|
let paths = collect_paths(workspace)?;
|
|
25
|
-
let
|
|
45
|
+
let log = EventLog::new(&paths.run_workspace);
|
|
46
|
+
let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
|
|
47
|
+
Some(team) => Some(resolve_owner_team_for_read(&paths.run_workspace, team, Some(&log))?),
|
|
48
|
+
None => None,
|
|
49
|
+
};
|
|
50
|
+
let owner_team_id = resolved_owner_team_id.as_deref();
|
|
51
|
+
let mut state = match owner_team_id {
|
|
52
|
+
Some(team) => crate::state::projection::select_runtime_state(&paths.run_workspace, Some(team))?,
|
|
53
|
+
None => crate::state::persist::load_runtime_state(&paths.run_workspace)?,
|
|
54
|
+
};
|
|
55
|
+
let spec_workspace = owner_team_id
|
|
56
|
+
.and_then(|_| state_spec_workspace_from_value(&state))
|
|
57
|
+
.unwrap_or_else(|| paths.spec_workspace.clone());
|
|
58
|
+
let spec_path = spec_workspace.join("team.spec.yaml");
|
|
26
59
|
if !spec_path.exists() {
|
|
27
60
|
return Err(MessagingError::Validation(format!("Cannot read {}", spec_path.display())));
|
|
28
61
|
}
|
|
29
62
|
let store = MessageStore::open(&paths.run_workspace)?;
|
|
30
63
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
31
64
|
if let Some(path) = result_file {
|
|
32
|
-
ingest_result_file(&conn, path)?;
|
|
65
|
+
ingest_result_file(&conn, path, owner_team_id)?;
|
|
33
66
|
}
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
let sql = match owner_team_id {
|
|
68
|
+
Some(_) => {
|
|
69
|
+
"select result_id, task_id, agent_id, envelope, status, created_at
|
|
70
|
+
from results
|
|
71
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
72
|
+
order by created_at, result_id"
|
|
73
|
+
}
|
|
74
|
+
None => {
|
|
75
|
+
"select result_id, task_id, agent_id, envelope, status, created_at
|
|
76
|
+
from results
|
|
77
|
+
where status not in ('collected', 'invalid')
|
|
78
|
+
order by created_at, result_id"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
let mut stmt = conn.prepare(sql)?;
|
|
82
|
+
let row_mapper = |row: &rusqlite::Row<'_>| {
|
|
42
83
|
Ok(StoredResult {
|
|
43
84
|
result_id: row.get(0)?,
|
|
44
85
|
task_id: row.get(1)?,
|
|
@@ -47,20 +88,24 @@ pub fn collect(
|
|
|
47
88
|
status: row.get(4)?,
|
|
48
89
|
created_at: row.get(5)?,
|
|
49
90
|
})
|
|
50
|
-
}
|
|
91
|
+
};
|
|
92
|
+
let rows = match owner_team_id {
|
|
93
|
+
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
94
|
+
None => stmt.query_map([], row_mapper),
|
|
95
|
+
}?
|
|
51
96
|
.collect::<Result<Vec<_>, _>>()?;
|
|
52
97
|
drop(stmt);
|
|
53
98
|
|
|
54
|
-
let mut state = crate::state::persist::load_runtime_state(&paths.run_workspace)?;
|
|
55
99
|
let mut collected = Vec::new();
|
|
56
100
|
let mut collected_results = Vec::new();
|
|
57
101
|
let mut invalid_results = Vec::new();
|
|
102
|
+
let mut fatal_invalid_results = 0usize;
|
|
58
103
|
let mut state_dirty = false;
|
|
59
|
-
let log = EventLog::new(&paths.run_workspace);
|
|
60
104
|
for row in rows {
|
|
61
105
|
let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
|
|
62
106
|
Ok(envelope) => envelope,
|
|
63
107
|
Err(error) => {
|
|
108
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
64
109
|
record_invalid_result(
|
|
65
110
|
&conn,
|
|
66
111
|
&mut invalid_results,
|
|
@@ -72,6 +117,7 @@ pub fn collect(
|
|
|
72
117
|
}
|
|
73
118
|
};
|
|
74
119
|
if let Err(error) = validate_result_envelope(&envelope) {
|
|
120
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
75
121
|
record_invalid_result(
|
|
76
122
|
&conn,
|
|
77
123
|
&mut invalid_results,
|
|
@@ -83,9 +129,12 @@ pub fn collect(
|
|
|
83
129
|
}
|
|
84
130
|
let scope = if task_exists(&state, &row.task_id) {
|
|
85
131
|
"task"
|
|
86
|
-
} else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id)? {
|
|
132
|
+
} else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id, owner_team_id)? {
|
|
87
133
|
"message"
|
|
88
134
|
} else {
|
|
135
|
+
if result_file.is_some() || row.task_id != "manual" {
|
|
136
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
137
|
+
}
|
|
89
138
|
record_invalid_result(
|
|
90
139
|
&conn,
|
|
91
140
|
&mut invalid_results,
|
|
@@ -95,10 +144,20 @@ pub fn collect(
|
|
|
95
144
|
)?;
|
|
96
145
|
continue;
|
|
97
146
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
147
|
+
match owner_team_id {
|
|
148
|
+
Some(team) => {
|
|
149
|
+
conn.execute(
|
|
150
|
+
"update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2",
|
|
151
|
+
params![row.result_id.as_str(), team],
|
|
152
|
+
)?;
|
|
153
|
+
}
|
|
154
|
+
None => {
|
|
155
|
+
conn.execute(
|
|
156
|
+
"update results set status = 'collected' where result_id = ?1",
|
|
157
|
+
params![row.result_id.as_str()],
|
|
158
|
+
)?;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
102
161
|
if scope == "task" {
|
|
103
162
|
mark_task_done(&mut state, &row.task_id, &row.result_id);
|
|
104
163
|
state_dirty = true;
|
|
@@ -129,17 +188,21 @@ pub fn collect(
|
|
|
129
188
|
collected_results.push(summary);
|
|
130
189
|
}
|
|
131
190
|
if state_dirty {
|
|
132
|
-
|
|
191
|
+
if owner_team_id.is_some() {
|
|
192
|
+
crate::state::projection::save_team_scoped_state(&paths.run_workspace, &state)?;
|
|
193
|
+
} else {
|
|
194
|
+
crate::state::persist::save_runtime_state(&paths.run_workspace, &state)?;
|
|
195
|
+
}
|
|
133
196
|
}
|
|
134
|
-
let counts = result_counts(&conn)?;
|
|
197
|
+
let counts = result_counts(&conn, owner_team_id)?;
|
|
135
198
|
Ok(serde_json::json!({
|
|
136
|
-
"ok":
|
|
199
|
+
"ok": fatal_invalid_results == 0,
|
|
137
200
|
"collected": collected,
|
|
138
201
|
"collected_results": collected_results,
|
|
139
202
|
"delivered_messages": [],
|
|
140
203
|
"invalid_results": invalid_results,
|
|
141
204
|
"results": counts,
|
|
142
|
-
"state_file":
|
|
205
|
+
"state_file": spec_workspace.join("team_state.md").to_string_lossy().to_string(),
|
|
143
206
|
"coordinator": {
|
|
144
207
|
"ok": false,
|
|
145
208
|
"status": "not_required",
|
|
@@ -147,12 +210,48 @@ pub fn collect(
|
|
|
147
210
|
}))
|
|
148
211
|
}
|
|
149
212
|
|
|
213
|
+
fn resolve_owner_team_for_read(
|
|
214
|
+
workspace: &Path,
|
|
215
|
+
requested: &str,
|
|
216
|
+
event_log: Option<&EventLog>,
|
|
217
|
+
) -> Result<String, MessagingError> {
|
|
218
|
+
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
219
|
+
match crate::state::projection::resolve_owner_team_id(&state, requested) {
|
|
220
|
+
OwnerTeamResolution::Canonical(canonical) => Ok(canonical),
|
|
221
|
+
OwnerTeamResolution::LegacyAlias { requested, canonical } => {
|
|
222
|
+
crate::messaging::delivery::normalize_owner_team_id_rows(
|
|
223
|
+
workspace,
|
|
224
|
+
&requested,
|
|
225
|
+
&canonical,
|
|
226
|
+
None,
|
|
227
|
+
event_log,
|
|
228
|
+
)?;
|
|
229
|
+
Ok(canonical)
|
|
230
|
+
}
|
|
231
|
+
OwnerTeamResolution::Unresolved { requested } => {
|
|
232
|
+
Err(MessagingError::Routing(format!("owner_team_unresolved: {requested}")))
|
|
233
|
+
}
|
|
234
|
+
OwnerTeamResolution::Ambiguous { requested, matches } => {
|
|
235
|
+
Err(MessagingError::Routing(format!(
|
|
236
|
+
"owner_team_ambiguous: {requested} matches {}",
|
|
237
|
+
matches.join(",")
|
|
238
|
+
)))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
150
243
|
struct CollectPaths {
|
|
151
244
|
run_workspace: PathBuf,
|
|
152
245
|
spec_workspace: PathBuf,
|
|
153
246
|
}
|
|
154
247
|
|
|
155
248
|
fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
|
|
249
|
+
if collect_input_has_no_local_team_context(workspace) {
|
|
250
|
+
return Ok(CollectPaths {
|
|
251
|
+
run_workspace: workspace.to_path_buf(),
|
|
252
|
+
spec_workspace: workspace.to_path_buf(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
156
255
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
157
256
|
.map_err(|e| MessagingError::Routing(e.to_string()))?;
|
|
158
257
|
let spec_workspace = if workspace.join("team.spec.yaml").exists() {
|
|
@@ -168,8 +267,24 @@ fn collect_paths(workspace: &Path) -> Result<CollectPaths, MessagingError> {
|
|
|
168
267
|
})
|
|
169
268
|
}
|
|
170
269
|
|
|
270
|
+
fn collect_input_has_no_local_team_context(workspace: &Path) -> bool {
|
|
271
|
+
!workspace.join("team.spec.yaml").exists()
|
|
272
|
+
&& !workspace.join(".team").exists()
|
|
273
|
+
&& !crate::state::persist::runtime_state_path(workspace).exists()
|
|
274
|
+
&& workspace.file_name().and_then(|s| s.to_str()) != Some(".team")
|
|
275
|
+
&& workspace
|
|
276
|
+
.parent()
|
|
277
|
+
.and_then(|p| p.file_name())
|
|
278
|
+
.and_then(|s| s.to_str())
|
|
279
|
+
!= Some(".team")
|
|
280
|
+
}
|
|
281
|
+
|
|
171
282
|
fn state_spec_workspace(run_workspace: &Path) -> Option<PathBuf> {
|
|
172
283
|
let state = crate::state::persist::load_runtime_state(run_workspace).ok()?;
|
|
284
|
+
state_spec_workspace_from_value(&state)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn state_spec_workspace_from_value(state: &serde_json::Value) -> Option<PathBuf> {
|
|
173
288
|
if let Some(spec_path) = state.get("spec_path").and_then(serde_json::Value::as_str) {
|
|
174
289
|
return PathBuf::from(spec_path).parent().map(Path::to_path_buf);
|
|
175
290
|
}
|
|
@@ -200,7 +315,11 @@ fn record_invalid_result(
|
|
|
200
315
|
Ok(())
|
|
201
316
|
}
|
|
202
317
|
|
|
203
|
-
fn ingest_result_file(
|
|
318
|
+
fn ingest_result_file(
|
|
319
|
+
conn: &rusqlite::Connection,
|
|
320
|
+
path: &Path,
|
|
321
|
+
owner_team_id: Option<&str>,
|
|
322
|
+
) -> Result<(), MessagingError> {
|
|
204
323
|
let raw = std::fs::read_to_string(path)?;
|
|
205
324
|
let mut envelope: serde_json::Value = serde_json::from_str(&raw)?;
|
|
206
325
|
validate_result_envelope(&envelope)?;
|
|
@@ -226,7 +345,7 @@ fn ingest_result_file(conn: &rusqlite::Connection, path: &Path) -> Result<(), Me
|
|
|
226
345
|
agent_id,
|
|
227
346
|
&envelope.to_string(),
|
|
228
347
|
status,
|
|
229
|
-
|
|
348
|
+
owner_team_id,
|
|
230
349
|
)?;
|
|
231
350
|
Ok(())
|
|
232
351
|
}
|
|
@@ -273,39 +392,55 @@ fn is_message_scoped_result(
|
|
|
273
392
|
conn: &rusqlite::Connection,
|
|
274
393
|
task_id: &str,
|
|
275
394
|
agent_id: &str,
|
|
395
|
+
owner_team_id: Option<&str>,
|
|
276
396
|
) -> Result<bool, MessagingError> {
|
|
277
397
|
if !task_id.starts_with("msg_") {
|
|
278
398
|
return Ok(false);
|
|
279
399
|
}
|
|
280
|
-
let count: i64 =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
400
|
+
let count: i64 = match owner_team_id {
|
|
401
|
+
Some(team) => conn.query_row(
|
|
402
|
+
"select count(*) from messages where message_id = ?1 and recipient = ?2 and owner_team_id = ?3",
|
|
403
|
+
params![task_id, agent_id, team],
|
|
404
|
+
|row| row.get(0),
|
|
405
|
+
)?,
|
|
406
|
+
None => conn.query_row(
|
|
407
|
+
"select count(*) from messages where message_id = ?1 and recipient = ?2",
|
|
408
|
+
params![task_id, agent_id],
|
|
409
|
+
|row| row.get(0),
|
|
410
|
+
)?,
|
|
411
|
+
};
|
|
285
412
|
Ok(count > 0)
|
|
286
413
|
}
|
|
287
414
|
|
|
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
|
-
)?;
|
|
415
|
+
fn result_counts(
|
|
416
|
+
conn: &rusqlite::Connection,
|
|
417
|
+
owner_team_id: Option<&str>,
|
|
418
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
419
|
+
let total: i64 = count_results(conn, owner_team_id, None)?;
|
|
420
|
+
let collected: i64 = count_results(conn, owner_team_id, Some("collected"))?;
|
|
421
|
+
let invalid: i64 = count_results(conn, owner_team_id, Some("invalid"))?;
|
|
300
422
|
let uncollected = total - collected - invalid;
|
|
301
423
|
let mut by_status = serde_json::Map::new();
|
|
302
|
-
let
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
424
|
+
let sql = match owner_team_id {
|
|
425
|
+
Some(_) => {
|
|
426
|
+
"select status, count(*) from results
|
|
427
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
428
|
+
group by status
|
|
429
|
+
order by status"
|
|
430
|
+
}
|
|
431
|
+
None => {
|
|
432
|
+
"select status, count(*) from results
|
|
433
|
+
where status not in ('collected', 'invalid')
|
|
434
|
+
group by status
|
|
435
|
+
order by status"
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
let mut stmt = conn.prepare(sql)?;
|
|
439
|
+
let row_mapper = |row: &rusqlite::Row<'_>| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?));
|
|
440
|
+
let rows = match owner_team_id {
|
|
441
|
+
Some(team) => stmt.query_map(params![team], row_mapper),
|
|
442
|
+
None => stmt.query_map([], row_mapper),
|
|
443
|
+
}?;
|
|
309
444
|
for row in rows {
|
|
310
445
|
let (status, count) = row?;
|
|
311
446
|
by_status.insert(status, serde_json::Value::Number(count.into()));
|
|
@@ -319,12 +454,45 @@ fn result_counts(conn: &rusqlite::Connection) -> Result<serde_json::Value, Messa
|
|
|
319
454
|
}))
|
|
320
455
|
}
|
|
321
456
|
|
|
457
|
+
fn count_results(
|
|
458
|
+
conn: &rusqlite::Connection,
|
|
459
|
+
owner_team_id: Option<&str>,
|
|
460
|
+
status: Option<&str>,
|
|
461
|
+
) -> Result<i64, MessagingError> {
|
|
462
|
+
match (owner_team_id, status) {
|
|
463
|
+
(Some(team), Some(status)) => Ok(conn.query_row(
|
|
464
|
+
"select count(*) from results where owner_team_id = ?1 and status = ?2",
|
|
465
|
+
params![team, status],
|
|
466
|
+
|row| row.get(0),
|
|
467
|
+
)?),
|
|
468
|
+
(Some(team), None) => Ok(conn.query_row(
|
|
469
|
+
"select count(*) from results where owner_team_id = ?1",
|
|
470
|
+
params![team],
|
|
471
|
+
|row| row.get(0),
|
|
472
|
+
)?),
|
|
473
|
+
(None, Some(status)) => Ok(conn.query_row(
|
|
474
|
+
"select count(*) from results where status = ?1",
|
|
475
|
+
params![status],
|
|
476
|
+
|row| row.get(0),
|
|
477
|
+
)?),
|
|
478
|
+
(None, None) => Ok(conn.query_row("select count(*) from results", [], |row| row.get(0))?),
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
322
482
|
/// `report_result` (`results.py:191`):worker 报结果 —— 校验 envelope、存 result、ack 任务消息、
|
|
323
483
|
/// **排队** send 事件通知 leader、推进 orchestrator (软依赖,失败仅记 `orchestrator.advance_skipped`)。
|
|
324
484
|
/// MCP `report_result` 工具调。
|
|
325
485
|
pub fn report_result(
|
|
326
486
|
workspace: &Path,
|
|
327
487
|
envelope: &serde_json::Value,
|
|
488
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
489
|
+
report_result_for_owner_team(workspace, envelope, None)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
pub fn report_result_for_owner_team(
|
|
493
|
+
workspace: &Path,
|
|
494
|
+
envelope: &serde_json::Value,
|
|
495
|
+
explicit_owner_team: Option<&str>,
|
|
328
496
|
) -> Result<serde_json::Value, MessagingError> {
|
|
329
497
|
validate_result_envelope(envelope)?;
|
|
330
498
|
let store = MessageStore::open(workspace)?;
|
|
@@ -344,7 +512,10 @@ pub fn report_result(
|
|
|
344
512
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
345
513
|
let state_for_owner = crate::state::persist::load_runtime_state(workspace)
|
|
346
514
|
.unwrap_or(serde_json::json!({}));
|
|
347
|
-
let owner_team =
|
|
515
|
+
let owner_team = explicit_owner_team
|
|
516
|
+
.filter(|team| !team.is_empty())
|
|
517
|
+
.map(str::to_string)
|
|
518
|
+
.unwrap_or_else(|| super::leader_receiver::active_team_key(workspace, &state_for_owner));
|
|
348
519
|
let inserted = insert_result_if_absent(
|
|
349
520
|
&conn,
|
|
350
521
|
&result_id,
|
|
@@ -360,7 +531,7 @@ pub fn report_result(
|
|
|
360
531
|
"mcp.report_result_duplicate_ignored",
|
|
361
532
|
serde_json::json!({
|
|
362
533
|
"notification_status": "duplicate_ignored",
|
|
363
|
-
"owner_team_id":
|
|
534
|
+
"owner_team_id": owner_team,
|
|
364
535
|
"result_id": result_id,
|
|
365
536
|
}),
|
|
366
537
|
)?;
|
|
@@ -392,7 +563,7 @@ pub fn report_result(
|
|
|
392
563
|
// legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
|
|
393
564
|
// to the caller as "success" while leader actually never saw the result text).
|
|
394
565
|
let content = format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
|
|
395
|
-
let state =
|
|
566
|
+
let state = report_owner_state(&state_for_owner, &owner_team);
|
|
396
567
|
let event_log = EventLog::new(workspace);
|
|
397
568
|
let mut outcome = super::leader_receiver::send_to_leader_receiver(
|
|
398
569
|
workspace,
|
|
@@ -405,19 +576,72 @@ pub fn report_result(
|
|
|
405
576
|
Some(&result_id),
|
|
406
577
|
&event_log,
|
|
407
578
|
)?;
|
|
408
|
-
if
|
|
409
|
-
if let Some(message_id) = outcome.message_id.clone() {
|
|
579
|
+
if let Some(message_id) = outcome.message_id.clone() {
|
|
410
580
|
let store = MessageStore::open(workspace)?;
|
|
411
581
|
let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
&message_id,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
582
|
+
let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
|
|
583
|
+
.unwrap_or_else(|_| state_for_owner.clone());
|
|
584
|
+
let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
|
|
585
|
+
for attempt in 0..3 {
|
|
586
|
+
let _ = store.mark(&message_id, "accepted", None);
|
|
587
|
+
outcome = super::delivery::deliver_pending_message(
|
|
588
|
+
workspace,
|
|
589
|
+
&store,
|
|
590
|
+
&transport,
|
|
591
|
+
&message_id,
|
|
592
|
+
&event_log,
|
|
593
|
+
&delivery_state,
|
|
594
|
+
)?;
|
|
595
|
+
if outcome.ok {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
let delivered = super::delivery::deliver_pending_messages(
|
|
599
|
+
workspace,
|
|
600
|
+
&delivery_state,
|
|
601
|
+
&transport,
|
|
602
|
+
&event_log,
|
|
603
|
+
)?;
|
|
604
|
+
if delivered.iter().any(|delivered_id| delivered_id == &message_id) {
|
|
605
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
606
|
+
ok: true,
|
|
607
|
+
status: crate::messaging::DeliveryStatus::Delivered,
|
|
608
|
+
message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
|
|
609
|
+
message_id: Some(message_id.clone()),
|
|
610
|
+
verification: None,
|
|
611
|
+
stage: None,
|
|
612
|
+
reason: None,
|
|
613
|
+
channel: Some("leader_receiver".to_string()),
|
|
614
|
+
};
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
if attempt < 2 {
|
|
618
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
match inject_leader_notification_direct(workspace, &delivery_state, &content, &message_id) {
|
|
622
|
+
Ok(()) => {
|
|
623
|
+
store.mark(&message_id, "delivered", None)?;
|
|
624
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
625
|
+
ok: true,
|
|
626
|
+
status: crate::messaging::DeliveryStatus::Delivered,
|
|
627
|
+
message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
|
|
628
|
+
message_id: Some(message_id),
|
|
629
|
+
verification: None,
|
|
630
|
+
stage: None,
|
|
631
|
+
reason: None,
|
|
632
|
+
channel: Some("leader_receiver".to_string()),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
Err(reason) => {
|
|
636
|
+
event_log.write(
|
|
637
|
+
"leader_receiver.direct_inject_skipped",
|
|
638
|
+
serde_json::json!({
|
|
639
|
+
"message_id": message_id,
|
|
640
|
+
"reason": reason,
|
|
641
|
+
}),
|
|
642
|
+
)?;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
421
645
|
}
|
|
422
646
|
let leader_notified = outcome.ok;
|
|
423
647
|
let notification_status_wire = if outcome.ok {
|
|
@@ -437,7 +661,7 @@ pub fn report_result(
|
|
|
437
661
|
"notification_channel": channel,
|
|
438
662
|
"notification_message_id": outcome.message_id,
|
|
439
663
|
"notification_status": notification_status_wire,
|
|
440
|
-
"owner_team_id":
|
|
664
|
+
"owner_team_id": owner_team,
|
|
441
665
|
"result_id": result_id,
|
|
442
666
|
}),
|
|
443
667
|
)?;
|
|
@@ -471,6 +695,72 @@ pub fn report_result(
|
|
|
471
695
|
Ok(serde_json::Value::Object(out))
|
|
472
696
|
}
|
|
473
697
|
|
|
698
|
+
fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
|
|
699
|
+
let mut state = match crate::state::projection::resolve_owner_team_id(state, owner_team)
|
|
700
|
+
.canonical_key()
|
|
701
|
+
{
|
|
702
|
+
Some(team) => crate::state::projection::project_top_level_view(state, team),
|
|
703
|
+
None => state.clone(),
|
|
704
|
+
};
|
|
705
|
+
if let Some(obj) = state.as_object_mut() {
|
|
706
|
+
obj.insert(
|
|
707
|
+
"active_team_key".to_string(),
|
|
708
|
+
serde_json::Value::String(owner_team.to_string()),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
state
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fn inject_leader_notification_direct(
|
|
715
|
+
workspace: &Path,
|
|
716
|
+
state: &serde_json::Value,
|
|
717
|
+
content: &str,
|
|
718
|
+
message_id: &str,
|
|
719
|
+
) -> Result<(), String> {
|
|
720
|
+
let Some(pane_id) = state
|
|
721
|
+
.get("leader_receiver")
|
|
722
|
+
.or_else(|| state.get("team_owner"))
|
|
723
|
+
.and_then(|receiver| receiver.get("pane_id"))
|
|
724
|
+
.and_then(serde_json::Value::as_str)
|
|
725
|
+
.filter(|pane| !pane.is_empty() && *pane != "__team_agent_unbound__")
|
|
726
|
+
else {
|
|
727
|
+
return Err("leader_direct_inject_failed:no_bound_pane".to_string());
|
|
728
|
+
};
|
|
729
|
+
let rendered = format!(
|
|
730
|
+
"Team Agent message from leader_receiver:\n\n{content}\n\n[team-agent-token:{message_id}]"
|
|
731
|
+
);
|
|
732
|
+
let target = Target::Pane(PaneId::new(pane_id));
|
|
733
|
+
if let Some(socket) = state
|
|
734
|
+
.get("leader_receiver")
|
|
735
|
+
.and_then(|receiver| receiver.get("tmux_socket"))
|
|
736
|
+
.and_then(serde_json::Value::as_str)
|
|
737
|
+
.filter(|socket| !socket.is_empty())
|
|
738
|
+
{
|
|
739
|
+
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
740
|
+
if backend
|
|
741
|
+
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
742
|
+
.is_ok()
|
|
743
|
+
{
|
|
744
|
+
return Ok(());
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
748
|
+
if workspace_backend
|
|
749
|
+
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
750
|
+
.is_ok()
|
|
751
|
+
{
|
|
752
|
+
return Ok(());
|
|
753
|
+
}
|
|
754
|
+
let default_backend = crate::tmux_backend::TmuxBackend::new();
|
|
755
|
+
if default_backend
|
|
756
|
+
.inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
|
|
757
|
+
.is_ok()
|
|
758
|
+
{
|
|
759
|
+
return Ok(());
|
|
760
|
+
}
|
|
761
|
+
Err(format!("leader_direct_inject_failed:pane={pane_id}"))
|
|
762
|
+
}
|
|
763
|
+
|
|
474
764
|
fn insert_result_if_absent(
|
|
475
765
|
conn: &rusqlite::Connection,
|
|
476
766
|
result_id: &str,
|