@team-agent/installer 0.5.60 → 0.5.61
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 +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/session/capture.rs +40 -18
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
- package/crates/team-agent/src/provider/session_scan.rs +5 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -3,6 +3,7 @@ use std::path::Path;
|
|
|
3
3
|
use serde_json::Value;
|
|
4
4
|
|
|
5
5
|
use crate::codex_app_server::AppServerBinding;
|
|
6
|
+
use crate::model::pane_authority_refusal::PaneWorkspaceMismatchFacts;
|
|
6
7
|
use crate::transport::{PaneInfo, Transport};
|
|
7
8
|
|
|
8
9
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -26,7 +27,7 @@ pub enum LeaderChannelResolution {
|
|
|
26
27
|
ProbeFailed(String),
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
#[derive(Debug, Clone,
|
|
30
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
30
31
|
pub enum LeaderChannelUnbound {
|
|
31
32
|
ReceiverNotAttached,
|
|
32
33
|
TransportConflict,
|
|
@@ -34,10 +35,25 @@ pub enum LeaderChannelUnbound {
|
|
|
34
35
|
NonCanonicalTmuxSocket,
|
|
35
36
|
EndpointMismatch,
|
|
36
37
|
PaneNotLive,
|
|
37
|
-
PaneWorkspaceMismatch,
|
|
38
|
+
PaneWorkspaceMismatch(PaneWorkspaceMismatchFacts),
|
|
38
39
|
AppServerBindingInvalid,
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
impl LeaderChannelUnbound {
|
|
43
|
+
pub const fn reason_code(&self) -> &'static str {
|
|
44
|
+
match self {
|
|
45
|
+
Self::ReceiverNotAttached => "ReceiverNotAttached",
|
|
46
|
+
Self::TransportConflict => "TransportConflict",
|
|
47
|
+
Self::MissingPaneId => "MissingPaneId",
|
|
48
|
+
Self::NonCanonicalTmuxSocket => "NonCanonicalTmuxSocket",
|
|
49
|
+
Self::EndpointMismatch => "EndpointMismatch",
|
|
50
|
+
Self::PaneNotLive => "PaneNotLive",
|
|
51
|
+
Self::PaneWorkspaceMismatch(_) => "PaneWorkspaceMismatch",
|
|
52
|
+
Self::AppServerBindingInvalid => "AppServerBindingInvalid",
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
/// Resolve the canonical receiver's live physical channel without mutating
|
|
42
58
|
/// ownership. For direct tmux, `(absolute socket, pane id)` plus an available
|
|
43
59
|
/// pane cwd inside the target workspace is the authority. Session/window/tty/
|
|
@@ -79,11 +95,12 @@ pub fn resolve_live_leader_channel(
|
|
|
79
95
|
.get("tmux_socket")
|
|
80
96
|
.and_then(Value::as_str)
|
|
81
97
|
.filter(|socket| !socket.is_empty());
|
|
98
|
+
let transport_endpoint = transport.tmux_endpoint();
|
|
82
99
|
if tmux_socket.is_some_and(|socket| !std::path::Path::new(socket).is_absolute()) {
|
|
83
100
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::NonCanonicalTmuxSocket);
|
|
84
101
|
}
|
|
85
102
|
if let Some(expected) = tmux_socket {
|
|
86
|
-
if
|
|
103
|
+
if transport_endpoint.as_deref() != Some(expected) {
|
|
87
104
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::EndpointMismatch);
|
|
88
105
|
}
|
|
89
106
|
}
|
|
@@ -97,13 +114,19 @@ pub fn resolve_live_leader_channel(
|
|
|
97
114
|
else {
|
|
98
115
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneNotLive);
|
|
99
116
|
};
|
|
100
|
-
if observed
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
if let Some(observed_pane_workspace) = observed.current_path.clone() {
|
|
118
|
+
if !path_is_in_workspace(&observed_pane_workspace, workspace)
|
|
119
|
+
&& !explicit_claim_authority_matches(workspace, receiver, &observed)
|
|
120
|
+
{
|
|
121
|
+
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(
|
|
122
|
+
PaneWorkspaceMismatchFacts {
|
|
123
|
+
requested_workspace: canonical_path(workspace),
|
|
124
|
+
observed_pane_id: observed.pane_id.as_str().to_string(),
|
|
125
|
+
observed_pane_workspace,
|
|
126
|
+
endpoint: transport_endpoint.unwrap_or_else(|| "default".to_string()),
|
|
127
|
+
},
|
|
128
|
+
));
|
|
129
|
+
}
|
|
107
130
|
}
|
|
108
131
|
let metadata_drift = receiver_metadata_drift(receiver, &observed);
|
|
109
132
|
LeaderChannelResolution::Live(LiveLeaderChannel::DirectTmux(DirectTmuxLeaderChannel {
|
|
@@ -397,29 +397,46 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
397
397
|
let readback_ok = super::delivery::pane_readback_verified(&report);
|
|
398
398
|
if submit_ok {
|
|
399
399
|
store.record_delivery_submission(message_id, readback_ok)?;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
400
|
+
match super::delivery::observe_leader_receipt(state, message_id) {
|
|
401
|
+
super::delivery::LeaderReceiptObservation::SourceUnavailable => {
|
|
402
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
403
|
+
event_log.write(
|
|
404
|
+
"leader_receiver.receipt_source_unavailable",
|
|
405
|
+
serde_json::json!({
|
|
406
|
+
"message_id": message_id,
|
|
407
|
+
"result_id": result_id,
|
|
408
|
+
"channel": "fallback_pane",
|
|
409
|
+
}),
|
|
410
|
+
)?;
|
|
411
|
+
return Ok(super::delivery::leader_receipt_source_unavailable_outcome(
|
|
412
|
+
message_id,
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
super::delivery::LeaderReceiptObservation::TokenAbsent => {
|
|
416
|
+
store.mark(message_id, "submitted_pending_acceptance", None)?;
|
|
417
|
+
event_log.write(
|
|
418
|
+
"leader_receiver.acceptance_pending",
|
|
419
|
+
serde_json::json!({
|
|
420
|
+
"message_id": message_id,
|
|
421
|
+
"result_id": result_id,
|
|
422
|
+
"reason": "provider_receipt_not_observed",
|
|
423
|
+
"channel": "fallback_pane",
|
|
424
|
+
}),
|
|
425
|
+
)?;
|
|
426
|
+
return Ok(DeliveryOutcome {
|
|
427
|
+
ok: true,
|
|
428
|
+
status: DeliveryStatus::RetryScheduled,
|
|
429
|
+
message_status: MessageStatusShadow(
|
|
430
|
+
"submitted_pending_acceptance".to_string(),
|
|
431
|
+
),
|
|
432
|
+
message_id: Some(message_id.to_string()),
|
|
433
|
+
verification: Some("provider_receipt_not_observed".to_string()),
|
|
434
|
+
stage: Some(DeliveryStage::Submit),
|
|
435
|
+
reason: None,
|
|
436
|
+
channel: Some("leader_acceptance_pending".to_string()),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
super::delivery::LeaderReceiptObservation::TokenObserved => {}
|
|
423
440
|
}
|
|
424
441
|
store.mark_delivered_with_receipt(message_id)?;
|
|
425
442
|
event_log.write(
|
|
@@ -98,6 +98,12 @@ pub struct PresentationDecision {
|
|
|
98
98
|
pub policy_version: String,
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
102
|
+
pub struct SendPresentationInput {
|
|
103
|
+
pub request: PresentationRequest,
|
|
104
|
+
pub deprecation: Option<String>,
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
impl Default for PresentationRequest {
|
|
102
108
|
fn default() -> Self {
|
|
103
109
|
Self {
|
|
@@ -177,6 +183,79 @@ pub fn normalize_report_presentation(
|
|
|
177
183
|
(request, None)
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
/// Normalize the one-bit send surface and the temporary S-011 compatibility
|
|
187
|
+
/// input into the existing presentation disposition primitive.
|
|
188
|
+
///
|
|
189
|
+
/// S-011 sensitive: if the compatibility period is ended by a hard-cut
|
|
190
|
+
/// decision, replace this table with one uniform legacy-input refusal.
|
|
191
|
+
pub fn normalize_send_presentation(
|
|
192
|
+
mailbox: Option<&Value>,
|
|
193
|
+
legacy_presentation: Option<&Value>,
|
|
194
|
+
) -> Result<SendPresentationInput, String> {
|
|
195
|
+
if mailbox.is_some() && legacy_presentation.is_some() {
|
|
196
|
+
return Err("mailbox_conflicts_with_deprecated_presentation".to_string());
|
|
197
|
+
}
|
|
198
|
+
if let Some(mailbox) = mailbox {
|
|
199
|
+
let Some(mailbox) = mailbox.as_bool() else {
|
|
200
|
+
return Err("mailbox_must_be_boolean".to_string());
|
|
201
|
+
};
|
|
202
|
+
return Ok(SendPresentationInput {
|
|
203
|
+
request: PresentationRequest {
|
|
204
|
+
sink: if mailbox {
|
|
205
|
+
PresentationSink::Casefile
|
|
206
|
+
} else {
|
|
207
|
+
PresentationSink::Leader
|
|
208
|
+
},
|
|
209
|
+
class: PresentationClass::Message,
|
|
210
|
+
case_id: None,
|
|
211
|
+
},
|
|
212
|
+
deprecation: None,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
let Some(legacy) = legacy_presentation else {
|
|
216
|
+
return Ok(SendPresentationInput {
|
|
217
|
+
request: PresentationRequest::default(),
|
|
218
|
+
deprecation: None,
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
let Some(object) = legacy.as_object() else {
|
|
222
|
+
return Err("malformed_presentation".to_string());
|
|
223
|
+
};
|
|
224
|
+
let Some(class) = object.get("class").and_then(Value::as_str) else {
|
|
225
|
+
return Err("missing_class".to_string());
|
|
226
|
+
};
|
|
227
|
+
let Some(class) = PresentationClass::parse(class) else {
|
|
228
|
+
return Err(format!("unknown_class:{class}"));
|
|
229
|
+
};
|
|
230
|
+
let sink = match class {
|
|
231
|
+
PresentationClass::Message | PresentationClass::Blocking | PresentationClass::Timeout => {
|
|
232
|
+
PresentationSink::Leader
|
|
233
|
+
}
|
|
234
|
+
PresentationClass::Progress => PresentationSink::Casefile,
|
|
235
|
+
PresentationClass::StageResult
|
|
236
|
+
| PresentationClass::StagePass
|
|
237
|
+
| PresentationClass::Bounce
|
|
238
|
+
| PresentationClass::FinalReview => {
|
|
239
|
+
return Err(format!(
|
|
240
|
+
"deprecated_message_class:{}_requires_result_route",
|
|
241
|
+
class.as_str()
|
|
242
|
+
));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
Ok(SendPresentationInput {
|
|
246
|
+
request: PresentationRequest {
|
|
247
|
+
sink,
|
|
248
|
+
class: PresentationClass::Message,
|
|
249
|
+
case_id: None,
|
|
250
|
+
},
|
|
251
|
+
deprecation: Some(format!(
|
|
252
|
+
"message-class={} is deprecated; use mailbox={} instead",
|
|
253
|
+
class.as_str(),
|
|
254
|
+
sink != PresentationSink::Leader
|
|
255
|
+
)),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
180
259
|
pub fn normalize_presentation(value: Option<&Value>) -> (PresentationRequest, Option<String>) {
|
|
181
260
|
let Some(value) = value else {
|
|
182
261
|
return (PresentationRequest::default(), None);
|
|
@@ -304,4 +383,34 @@ mod tests {
|
|
|
304
383
|
"send normalization remains unchanged"
|
|
305
384
|
);
|
|
306
385
|
}
|
|
386
|
+
|
|
387
|
+
#[test]
|
|
388
|
+
fn send_mailbox_and_s011_compatibility_map_to_one_bit() {
|
|
389
|
+
let mailbox = normalize_send_presentation(Some(&json!(true)), None).unwrap();
|
|
390
|
+
assert_eq!(mailbox.request.sink, PresentationSink::Casefile);
|
|
391
|
+
assert_eq!(mailbox.deprecation, None);
|
|
392
|
+
|
|
393
|
+
for (class, sink) in [
|
|
394
|
+
("message", PresentationSink::Leader),
|
|
395
|
+
("progress", PresentationSink::Casefile),
|
|
396
|
+
("blocking", PresentationSink::Leader),
|
|
397
|
+
("timeout", PresentationSink::Leader),
|
|
398
|
+
] {
|
|
399
|
+
let legacy = json!({"sink": "silent", "class": class});
|
|
400
|
+
let mapped = normalize_send_presentation(None, Some(&legacy)).unwrap();
|
|
401
|
+
assert_eq!(mapped.request.sink, sink, "{class}");
|
|
402
|
+
assert!(mapped.deprecation.is_some(), "{class}");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn s011_stage_classes_require_result_route() {
|
|
408
|
+
for class in ["stage_result", "stage_pass", "bounce", "final_review"] {
|
|
409
|
+
let legacy = json!({"sink": "leader", "class": class});
|
|
410
|
+
assert_eq!(
|
|
411
|
+
normalize_send_presentation(None, Some(&legacy)).unwrap_err(),
|
|
412
|
+
format!("deprecated_message_class:{class}_requires_result_route")
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
307
416
|
}
|
|
@@ -14,6 +14,95 @@ use super::MessagingError;
|
|
|
14
14
|
use crate::model::ids::TaskId;
|
|
15
15
|
use crate::state::projection::OwnerTeamResolution;
|
|
16
16
|
|
|
17
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
18
|
+
pub enum ResultRoute {
|
|
19
|
+
Leader,
|
|
20
|
+
Pipeline,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl ResultRoute {
|
|
24
|
+
pub const ALL: [Self; 2] = [Self::Leader, Self::Pipeline];
|
|
25
|
+
|
|
26
|
+
pub const fn as_str(self) -> &'static str {
|
|
27
|
+
match self {
|
|
28
|
+
Self::Leader => "leader",
|
|
29
|
+
Self::Pipeline => "pipeline",
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn parse(value: &str) -> Option<Self> {
|
|
34
|
+
match value {
|
|
35
|
+
"leader" => Some(Self::Leader),
|
|
36
|
+
"pipeline" => Some(Self::Pipeline),
|
|
37
|
+
_ => None,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl Default for ResultRoute {
|
|
43
|
+
fn default() -> Self {
|
|
44
|
+
Self::Leader
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn results_for_case(
|
|
49
|
+
workspace: &Path,
|
|
50
|
+
case_id: &str,
|
|
51
|
+
owner_team_id: Option<&str>,
|
|
52
|
+
requested_owner_team_id: Option<&str>,
|
|
53
|
+
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
54
|
+
let current_db = workspace.join(".team/runtime/team.db");
|
|
55
|
+
let legacy_db = workspace.join(".team/team.db");
|
|
56
|
+
if current_db.exists() {
|
|
57
|
+
return results_for_case_in_db(
|
|
58
|
+
¤t_db,
|
|
59
|
+
case_id,
|
|
60
|
+
owner_team_id,
|
|
61
|
+
requested_owner_team_id,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if legacy_db.exists() {
|
|
65
|
+
return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
|
|
66
|
+
}
|
|
67
|
+
Ok(Vec::new())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn results_for_case_in_db(
|
|
71
|
+
db_path: &Path,
|
|
72
|
+
case_id: &str,
|
|
73
|
+
owner_team_id: Option<&str>,
|
|
74
|
+
requested_owner_team_id: Option<&str>,
|
|
75
|
+
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
76
|
+
let conn = crate::db::schema::open_db(db_path)?;
|
|
77
|
+
// The second predicate works around
|
|
78
|
+
// `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
|
|
79
|
+
// it is not design intent.
|
|
80
|
+
let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
|
|
81
|
+
let sql = match owner_team_id {
|
|
82
|
+
Some(_) => format!(
|
|
83
|
+
"select envelope from results where ({case_predicate}) \
|
|
84
|
+
and (owner_team_id = ?2 or owner_team_id = ?3) \
|
|
85
|
+
order by created_at, result_id"
|
|
86
|
+
),
|
|
87
|
+
None => format!(
|
|
88
|
+
"select envelope from results where {case_predicate} order by created_at, result_id"
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
let mut stmt = conn.prepare(&sql)?;
|
|
92
|
+
let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
|
|
93
|
+
let rows = match (owner_team_id, requested_owner_team_id) {
|
|
94
|
+
(Some(team), Some(requested_team)) => {
|
|
95
|
+
stmt.query_map(params![case_id, team, requested_team], row_mapper)
|
|
96
|
+
}
|
|
97
|
+
(Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
|
|
98
|
+
(None, _) => stmt.query_map(params![case_id], row_mapper),
|
|
99
|
+
}?
|
|
100
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
101
|
+
rows.into_iter()
|
|
102
|
+
.map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
|
|
103
|
+
.collect()
|
|
104
|
+
}
|
|
105
|
+
|
|
17
106
|
/// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
|
|
18
107
|
/// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
|
|
19
108
|
pub fn collect(
|
|
@@ -627,18 +716,12 @@ fn report_result_for_owner_team_inner(
|
|
|
627
716
|
explicit_owner_team: Option<&str>,
|
|
628
717
|
fallback_primary_error: Option<&str>,
|
|
629
718
|
) -> Result<serde_json::Value, MessagingError> {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return Err(MessagingError::Validation(format!(
|
|
635
|
-
"invalid presentation: {error}"
|
|
636
|
-
)));
|
|
719
|
+
if envelope.get("result_route").is_some() {
|
|
720
|
+
return Err(MessagingError::Validation(
|
|
721
|
+
"report_result result_route is task-owned and cannot be set by a worker".to_string(),
|
|
722
|
+
));
|
|
637
723
|
}
|
|
638
|
-
|
|
639
|
-
&presentation_request,
|
|
640
|
-
super::presentation::PresentationSource::ReportResult,
|
|
641
|
-
);
|
|
724
|
+
validate_result_envelope(envelope)?;
|
|
642
725
|
let store = MessageStore::open(workspace)?;
|
|
643
726
|
let result_id = envelope
|
|
644
727
|
.get("result_id")
|
|
@@ -656,12 +739,6 @@ fn report_result_for_owner_team_inner(
|
|
|
656
739
|
);
|
|
657
740
|
}
|
|
658
741
|
}
|
|
659
|
-
if let Some(obj) = stored.as_object_mut() {
|
|
660
|
-
obj.insert(
|
|
661
|
-
"presentation".to_string(),
|
|
662
|
-
serde_json::to_value(&presentation)?,
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
742
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
666
743
|
let state_for_owner =
|
|
667
744
|
crate::state::persist::load_runtime_state(workspace).unwrap_or(serde_json::json!({}));
|
|
@@ -669,6 +746,42 @@ fn report_result_for_owner_team_inner(
|
|
|
669
746
|
.filter(|team| !team.is_empty())
|
|
670
747
|
.map(str::to_string)
|
|
671
748
|
.unwrap_or_else(|| super::leader_receiver::active_team_key(workspace, &state_for_owner));
|
|
749
|
+
let (result_route, route_declared) = task_result_route(&state_for_owner, &owner_team, task_id)?;
|
|
750
|
+
let presentation = if route_declared {
|
|
751
|
+
let request = super::presentation::PresentationRequest {
|
|
752
|
+
sink: match result_route {
|
|
753
|
+
ResultRoute::Leader => super::presentation::PresentationSink::Leader,
|
|
754
|
+
ResultRoute::Pipeline => super::presentation::PresentationSink::Casefile,
|
|
755
|
+
},
|
|
756
|
+
class: match result_route {
|
|
757
|
+
ResultRoute::Leader => super::presentation::PresentationClass::Message,
|
|
758
|
+
ResultRoute::Pipeline => super::presentation::PresentationClass::StageResult,
|
|
759
|
+
},
|
|
760
|
+
case_id: (result_route == ResultRoute::Pipeline).then(|| task_id.to_string()),
|
|
761
|
+
};
|
|
762
|
+
super::presentation::decide_presentation(
|
|
763
|
+
&request,
|
|
764
|
+
super::presentation::PresentationSource::ReportResult,
|
|
765
|
+
)
|
|
766
|
+
} else {
|
|
767
|
+
let (request, error) =
|
|
768
|
+
super::presentation::normalize_report_presentation(envelope.get("presentation"));
|
|
769
|
+
if let Some(error) = error {
|
|
770
|
+
return Err(MessagingError::Validation(format!(
|
|
771
|
+
"invalid presentation: {error}"
|
|
772
|
+
)));
|
|
773
|
+
}
|
|
774
|
+
super::presentation::decide_presentation(
|
|
775
|
+
&request,
|
|
776
|
+
super::presentation::PresentationSource::ReportResult,
|
|
777
|
+
)
|
|
778
|
+
};
|
|
779
|
+
if let Some(obj) = stored.as_object_mut() {
|
|
780
|
+
obj.insert(
|
|
781
|
+
"presentation".to_string(),
|
|
782
|
+
serde_json::to_value(&presentation)?,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
672
785
|
let inserted = insert_result_if_absent(
|
|
673
786
|
&conn,
|
|
674
787
|
&result_id,
|
|
@@ -968,6 +1081,8 @@ fn report_result_for_owner_team_inner(
|
|
|
968
1081
|
let leader_notified = matches!(outcome.status, crate::messaging::DeliveryStatus::Delivered);
|
|
969
1082
|
let notification_status_wire = if leader_notified {
|
|
970
1083
|
"delivered"
|
|
1084
|
+
} else if outcome.channel.as_deref() == Some("leader_receipt_source_unavailable") {
|
|
1085
|
+
"injected_awaiting_receipt"
|
|
971
1086
|
} else if outcome.channel.as_deref() == Some("leader_acceptance_pending") {
|
|
972
1087
|
"submitted_pending_acceptance"
|
|
973
1088
|
} else if outcome.channel.as_deref() == Some("rebind_required")
|
|
@@ -1045,6 +1160,39 @@ fn report_result_for_owner_team_inner(
|
|
|
1045
1160
|
Ok(serde_json::Value::Object(out))
|
|
1046
1161
|
}
|
|
1047
1162
|
|
|
1163
|
+
fn task_result_route(
|
|
1164
|
+
state: &serde_json::Value,
|
|
1165
|
+
owner_team: &str,
|
|
1166
|
+
task_id: &str,
|
|
1167
|
+
) -> Result<(ResultRoute, bool), MessagingError> {
|
|
1168
|
+
let scoped_tasks = state
|
|
1169
|
+
.get("teams")
|
|
1170
|
+
.and_then(|teams| teams.get(owner_team))
|
|
1171
|
+
.and_then(|team| team.get("tasks"))
|
|
1172
|
+
.and_then(serde_json::Value::as_array);
|
|
1173
|
+
let top_level_tasks = state.get("tasks").and_then(serde_json::Value::as_array);
|
|
1174
|
+
let task = scoped_tasks
|
|
1175
|
+
.into_iter()
|
|
1176
|
+
.flatten()
|
|
1177
|
+
.chain(top_level_tasks.into_iter().flatten())
|
|
1178
|
+
.find(|task| task.get("id").and_then(serde_json::Value::as_str) == Some(task_id));
|
|
1179
|
+
let Some(route) = task.and_then(|task| task.get("result_route")) else {
|
|
1180
|
+
return Ok((ResultRoute::default(), false));
|
|
1181
|
+
};
|
|
1182
|
+
let route = route.as_str().ok_or_else(|| {
|
|
1183
|
+
MessagingError::Validation(format!(
|
|
1184
|
+
"task {task_id} has non-string result_route and cannot accept results"
|
|
1185
|
+
))
|
|
1186
|
+
})?;
|
|
1187
|
+
ResultRoute::parse(route)
|
|
1188
|
+
.map(|route| (route, true))
|
|
1189
|
+
.ok_or_else(|| {
|
|
1190
|
+
MessagingError::Validation(format!(
|
|
1191
|
+
"task {task_id} has unknown result_route {route:?} and cannot accept results"
|
|
1192
|
+
))
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1048
1196
|
fn copy_report_attribution_fields(
|
|
1049
1197
|
envelope: &serde_json::Value,
|
|
1050
1198
|
out: &mut serde_json::Map<String, serde_json::Value>,
|