@team-agent/installer 0.3.1 → 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 +7 -6
- package/crates/team-agent/src/cli/mod.rs +623 -21
- 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 +9 -0
- package/crates/team-agent/src/lifecycle/launch.rs +271 -58
- 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/messaging/delivery.rs +397 -36
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/results.rs +200 -47
- 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/persist.rs +113 -1
- package/crates/team-agent/src/state/projection.rs +127 -3
- package/crates/team-agent/src/tmux_backend.rs +66 -6
- package/package.json +4 -4
|
@@ -9,13 +9,17 @@ use crate::event_log::EventLog;
|
|
|
9
9
|
use crate::message_store::MessageStore;
|
|
10
10
|
use crate::model::enums::{PaneLiveness, Provider};
|
|
11
11
|
use crate::model::ids::TeamKey;
|
|
12
|
-
use crate::transport::{
|
|
12
|
+
use crate::transport::{
|
|
13
|
+
submit_verification_wire, InjectPayload, InjectReport, Key, PaneId, SessionName,
|
|
14
|
+
SubmitVerification, Target, Transport, WindowName,
|
|
15
|
+
};
|
|
13
16
|
|
|
14
17
|
use super::helpers::{message_exists, MessageStatusShadow};
|
|
15
18
|
use super::{
|
|
16
19
|
DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError,
|
|
17
20
|
PaneWidthQuery, TrustRetryPayload,
|
|
18
21
|
};
|
|
22
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
19
23
|
|
|
20
24
|
// ===========================================================================
|
|
21
25
|
// internal_delivery.py — coordinator/调度器侧 thin wrapper (card §65)
|
|
@@ -135,6 +139,21 @@ pub fn deliver_pending_message(
|
|
|
135
139
|
channel: None,
|
|
136
140
|
});
|
|
137
141
|
};
|
|
142
|
+
let mut canonical_owner_team_id = message.owner_team_id.clone();
|
|
143
|
+
let scoped_state;
|
|
144
|
+
let state = match message.owner_team_id.as_deref() {
|
|
145
|
+
Some(team) if !team.is_empty() => {
|
|
146
|
+
match project_state_for_owner_team(workspace, team, state, Some(store), Some(message_id), Some(event_log))? {
|
|
147
|
+
OwnerTeamProjection::Projected { state, canonical_team } => {
|
|
148
|
+
canonical_owner_team_id = Some(canonical_team);
|
|
149
|
+
scoped_state = state;
|
|
150
|
+
&scoped_state
|
|
151
|
+
}
|
|
152
|
+
OwnerTeamProjection::Refused(outcome) => return Ok(outcome),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_ => state,
|
|
156
|
+
};
|
|
138
157
|
if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
|
|
139
158
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
140
159
|
event_log.write(
|
|
@@ -225,39 +244,69 @@ pub fn deliver_pending_message(
|
|
|
225
244
|
&message.content,
|
|
226
245
|
message_id,
|
|
227
246
|
);
|
|
228
|
-
|
|
247
|
+
let rendered_len = rendered.len();
|
|
248
|
+
let inject_report = match transport.inject(
|
|
229
249
|
&target,
|
|
230
250
|
&InjectPayload::Text(rendered),
|
|
231
251
|
Key::Enter,
|
|
232
252
|
true,
|
|
233
253
|
) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
Ok(report) => report,
|
|
255
|
+
Err(error) => {
|
|
256
|
+
if message.recipient == "leader" {
|
|
257
|
+
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
258
|
+
event_log.write(
|
|
259
|
+
"leader_receiver.delivery_blocked",
|
|
260
|
+
serde_json::json!({
|
|
261
|
+
"message_id": message_id,
|
|
262
|
+
"sender": message.sender,
|
|
263
|
+
"reason": "leader_not_attached",
|
|
264
|
+
"channel": "rebind_required",
|
|
265
|
+
"action": "run team-agent claim-leader or team-agent takeover",
|
|
266
|
+
"error": error.to_string(),
|
|
267
|
+
}),
|
|
268
|
+
)?;
|
|
269
|
+
return Ok(DeliveryOutcome {
|
|
270
|
+
ok: false,
|
|
271
|
+
status: DeliveryStatus::Refused,
|
|
272
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
273
|
+
message_id: Some(message_id.to_string()),
|
|
274
|
+
verification: Some(
|
|
275
|
+
"run team-agent claim-leader or team-agent takeover".to_string(),
|
|
276
|
+
),
|
|
277
|
+
stage: None,
|
|
278
|
+
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
279
|
+
channel: Some("rebind_required".to_string()),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return Err(error.into());
|
|
259
283
|
}
|
|
260
|
-
|
|
284
|
+
};
|
|
285
|
+
if !inject_submit_verified(&inject_report, rendered_len, &message.sender, &message.recipient) {
|
|
286
|
+
let reason = format!(
|
|
287
|
+
"submit_unverified:{}",
|
|
288
|
+
submit_verification_wire(inject_report.submit_verification)
|
|
289
|
+
);
|
|
290
|
+
store.mark(message_id, "submitted_unverified", Some(&reason))?;
|
|
291
|
+
event_log.write(
|
|
292
|
+
"send.unverified",
|
|
293
|
+
serde_json::json!({
|
|
294
|
+
"message_id": message_id,
|
|
295
|
+
"recipient": message.recipient,
|
|
296
|
+
"reason": reason,
|
|
297
|
+
"attempts": inject_report.attempts,
|
|
298
|
+
}),
|
|
299
|
+
)?;
|
|
300
|
+
return Ok(DeliveryOutcome {
|
|
301
|
+
ok: false,
|
|
302
|
+
status: DeliveryStatus::Failed,
|
|
303
|
+
message_status: MessageStatusShadow("submitted_unverified".to_string()),
|
|
304
|
+
message_id: Some(message_id.to_string()),
|
|
305
|
+
verification: Some(reason),
|
|
306
|
+
stage: Some(DeliveryStage::Submit),
|
|
307
|
+
reason: None,
|
|
308
|
+
channel: None,
|
|
309
|
+
});
|
|
261
310
|
}
|
|
262
311
|
store.mark(message_id, "delivered", None)?;
|
|
263
312
|
event_log.write(
|
|
@@ -274,18 +323,39 @@ pub fn deliver_pending_message(
|
|
|
274
323
|
reason: None,
|
|
275
324
|
channel: None,
|
|
276
325
|
};
|
|
277
|
-
|
|
278
|
-
|
|
326
|
+
stamp_first_send_at_if_leader_to_worker_scoped(
|
|
327
|
+
workspace,
|
|
328
|
+
&message.sender,
|
|
329
|
+
&message.recipient,
|
|
330
|
+
canonical_owner_team_id.as_deref(),
|
|
331
|
+
)?;
|
|
332
|
+
record_turn_open_if_leader_to_worker_scoped(
|
|
279
333
|
workspace,
|
|
280
|
-
state,
|
|
281
334
|
&message.sender,
|
|
282
335
|
&message.recipient,
|
|
283
336
|
&outcome,
|
|
284
337
|
event_log,
|
|
338
|
+
canonical_owner_team_id.as_deref(),
|
|
285
339
|
)?;
|
|
286
340
|
Ok(outcome)
|
|
287
341
|
}
|
|
288
342
|
|
|
343
|
+
fn inject_submit_verified(
|
|
344
|
+
report: &InjectReport,
|
|
345
|
+
payload_len: usize,
|
|
346
|
+
sender: &str,
|
|
347
|
+
recipient: &str,
|
|
348
|
+
) -> bool {
|
|
349
|
+
match report.submit_verification {
|
|
350
|
+
SubmitVerification::SendKeysFailed => false,
|
|
351
|
+
SubmitVerification::PastedContentPromptAbsentAfterSubmit => true,
|
|
352
|
+
SubmitVerification::KeySentAfterVisibleToken { .. } => true,
|
|
353
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck => {
|
|
354
|
+
recipient == "leader" || matches!(sender, "leader" | "Leader") || payload_len < 80
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
289
359
|
/// Render a message into the worker-facing protocol block (port of `rust_core.py:render_message`,
|
|
290
360
|
/// golden-verified): `Team Agent message from {sender}[ for {task_id}]:\n\n{content}\n\n
|
|
291
361
|
/// [team-agent-token:{message_id}]`. The worker (fake or real provider) only builds a result_envelope
|
|
@@ -467,6 +537,19 @@ pub fn deliver_pending_messages(
|
|
|
467
537
|
let mut delivered = Vec::new();
|
|
468
538
|
for message_id in message_ids {
|
|
469
539
|
if let Some(message) = message_for_delivery(&store, &message_id)? {
|
|
540
|
+
let scoped_state;
|
|
541
|
+
let state = match message.owner_team_id.as_deref() {
|
|
542
|
+
Some(team) if !team.is_empty() => {
|
|
543
|
+
match project_state_for_owner_team(workspace, team, state, Some(&store), Some(&message_id), Some(event_log))? {
|
|
544
|
+
OwnerTeamProjection::Projected { state, .. } => {
|
|
545
|
+
scoped_state = state;
|
|
546
|
+
&scoped_state
|
|
547
|
+
}
|
|
548
|
+
OwnerTeamProjection::Refused(_) => continue,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
_ => state,
|
|
552
|
+
};
|
|
470
553
|
if recipient_is_busy(state, &message.recipient) {
|
|
471
554
|
event_log.write(
|
|
472
555
|
"send.deferred_busy",
|
|
@@ -493,6 +576,7 @@ struct PendingMessage {
|
|
|
493
576
|
recipient: String,
|
|
494
577
|
content: String,
|
|
495
578
|
task_id: Option<String>,
|
|
579
|
+
owner_team_id: Option<String>,
|
|
496
580
|
}
|
|
497
581
|
|
|
498
582
|
fn message_for_delivery(
|
|
@@ -502,7 +586,7 @@ fn message_for_delivery(
|
|
|
502
586
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
503
587
|
let message = conn
|
|
504
588
|
.query_row(
|
|
505
|
-
"select sender, recipient, content, task_id from messages where message_id = ?1",
|
|
589
|
+
"select sender, recipient, content, task_id, owner_team_id from messages where message_id = ?1",
|
|
506
590
|
params![message_id],
|
|
507
591
|
|row| {
|
|
508
592
|
Ok(PendingMessage {
|
|
@@ -510,6 +594,7 @@ fn message_for_delivery(
|
|
|
510
594
|
recipient: row.get::<_, String>(1)?,
|
|
511
595
|
content: row.get::<_, String>(2)?,
|
|
512
596
|
task_id: row.get::<_, Option<String>>(3)?,
|
|
597
|
+
owner_team_id: row.get::<_, Option<String>>(4)?,
|
|
513
598
|
})
|
|
514
599
|
},
|
|
515
600
|
)
|
|
@@ -666,10 +751,28 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
666
751
|
event_log: &EventLog,
|
|
667
752
|
) -> Result<(), MessagingError> {
|
|
668
753
|
let _ = state;
|
|
754
|
+
record_turn_open_if_leader_to_worker_scoped(
|
|
755
|
+
workspace,
|
|
756
|
+
sender,
|
|
757
|
+
recipient,
|
|
758
|
+
delivered,
|
|
759
|
+
event_log,
|
|
760
|
+
None,
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
fn record_turn_open_if_leader_to_worker_scoped(
|
|
765
|
+
workspace: &Path,
|
|
766
|
+
sender: &str,
|
|
767
|
+
recipient: &str,
|
|
768
|
+
delivered: &DeliveryOutcome,
|
|
769
|
+
event_log: &EventLog,
|
|
770
|
+
owner_team_id: Option<&str>,
|
|
771
|
+
) -> Result<(), MessagingError> {
|
|
669
772
|
if !delivered.ok || !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
670
773
|
return Ok(());
|
|
671
774
|
}
|
|
672
|
-
let mut state =
|
|
775
|
+
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
673
776
|
let Some(root) = state.as_object_mut() else {
|
|
674
777
|
return Ok(());
|
|
675
778
|
};
|
|
@@ -682,7 +785,7 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
682
785
|
serde_json::json!({"armed": true, "node_id": recipient, "turn_id": delivered.message_id}),
|
|
683
786
|
);
|
|
684
787
|
}
|
|
685
|
-
|
|
788
|
+
save_scoped_state(workspace, &state, owner_team_id)?;
|
|
686
789
|
event_log.write(
|
|
687
790
|
"turn_open.armed_after_delivery",
|
|
688
791
|
serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id}),
|
|
@@ -699,10 +802,19 @@ pub fn stamp_first_send_at_if_leader_to_worker(
|
|
|
699
802
|
recipient: &str,
|
|
700
803
|
) -> Result<(), MessagingError> {
|
|
701
804
|
let _ = state;
|
|
805
|
+
stamp_first_send_at_if_leader_to_worker_scoped(workspace, sender, recipient, None)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
fn stamp_first_send_at_if_leader_to_worker_scoped(
|
|
809
|
+
workspace: &Path,
|
|
810
|
+
sender: &str,
|
|
811
|
+
recipient: &str,
|
|
812
|
+
owner_team_id: Option<&str>,
|
|
813
|
+
) -> Result<(), MessagingError> {
|
|
702
814
|
if !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
703
815
|
return Ok(());
|
|
704
816
|
}
|
|
705
|
-
let mut state =
|
|
817
|
+
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
706
818
|
let now = chrono::Utc::now().to_rfc3339();
|
|
707
819
|
if let Some(agent) = state
|
|
708
820
|
.get_mut("agents")
|
|
@@ -712,12 +824,261 @@ pub fn stamp_first_send_at_if_leader_to_worker(
|
|
|
712
824
|
{
|
|
713
825
|
if !agent.contains_key("first_send_at") || agent.get("first_send_at").is_some_and(serde_json::Value::is_null) {
|
|
714
826
|
agent.insert("first_send_at".to_string(), serde_json::Value::String(now));
|
|
715
|
-
|
|
827
|
+
save_scoped_state(workspace, &state, owner_team_id)?;
|
|
716
828
|
}
|
|
717
829
|
}
|
|
718
830
|
Ok(())
|
|
719
831
|
}
|
|
720
832
|
|
|
833
|
+
fn scoped_state_for_write(
|
|
834
|
+
workspace: &Path,
|
|
835
|
+
owner_team_id: Option<&str>,
|
|
836
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
837
|
+
match owner_team_id.filter(|team| !team.is_empty()) {
|
|
838
|
+
Some(team) => {
|
|
839
|
+
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
840
|
+
match project_state_for_owner_team_value(&raw, team) {
|
|
841
|
+
Some(projected) => Ok(projected),
|
|
842
|
+
None => Ok(raw),
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
None => Ok(crate::state::persist::load_runtime_state(workspace)?),
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
fn save_scoped_state(
|
|
850
|
+
workspace: &Path,
|
|
851
|
+
state: &serde_json::Value,
|
|
852
|
+
owner_team_id: Option<&str>,
|
|
853
|
+
) -> Result<(), MessagingError> {
|
|
854
|
+
if owner_team_id.filter(|team| !team.is_empty()).is_some() {
|
|
855
|
+
if state
|
|
856
|
+
.get("teams")
|
|
857
|
+
.and_then(serde_json::Value::as_object)
|
|
858
|
+
.is_some_and(|teams| {
|
|
859
|
+
owner_team_id
|
|
860
|
+
.and_then(|team| crate::state::projection::resolve_owner_team_id(state, team).canonical_key().map(str::to_string))
|
|
861
|
+
.is_some_and(|team| teams.contains_key(&team))
|
|
862
|
+
})
|
|
863
|
+
{
|
|
864
|
+
crate::state::projection::save_team_scoped_state(workspace, state)?;
|
|
865
|
+
} else {
|
|
866
|
+
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
870
|
+
}
|
|
871
|
+
Ok(())
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
enum OwnerTeamProjection {
|
|
875
|
+
Projected { state: serde_json::Value, canonical_team: String },
|
|
876
|
+
Refused(DeliveryOutcome),
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn project_state_for_owner_team(
|
|
880
|
+
workspace: &Path,
|
|
881
|
+
team: &str,
|
|
882
|
+
fallback: &serde_json::Value,
|
|
883
|
+
store: Option<&MessageStore>,
|
|
884
|
+
message_id: Option<&str>,
|
|
885
|
+
event_log: Option<&EventLog>,
|
|
886
|
+
) -> Result<OwnerTeamProjection, MessagingError> {
|
|
887
|
+
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
888
|
+
let fallback_has_teams = fallback
|
|
889
|
+
.get("teams")
|
|
890
|
+
.and_then(serde_json::Value::as_object)
|
|
891
|
+
.is_some_and(|teams| !teams.is_empty());
|
|
892
|
+
let (mut projection_source, mut resolution) = if fallback_has_teams {
|
|
893
|
+
(fallback, crate::state::projection::resolve_owner_team_id(fallback, team))
|
|
894
|
+
} else {
|
|
895
|
+
(&raw, crate::state::projection::resolve_owner_team_id(&raw, team))
|
|
896
|
+
};
|
|
897
|
+
if !fallback_has_teams && matches!(resolution, OwnerTeamResolution::Unresolved { .. }) {
|
|
898
|
+
let fallback_resolution = crate::state::projection::resolve_owner_team_id(fallback, team);
|
|
899
|
+
if !matches!(fallback_resolution, OwnerTeamResolution::Unresolved { .. }) {
|
|
900
|
+
resolution = fallback_resolution;
|
|
901
|
+
projection_source = fallback;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
let canonical_team = match resolution {
|
|
905
|
+
OwnerTeamResolution::Canonical(canonical) => canonical,
|
|
906
|
+
OwnerTeamResolution::LegacyAlias { requested, canonical } => {
|
|
907
|
+
normalize_owner_team_id_rows(workspace, &requested, &canonical, message_id, event_log)?;
|
|
908
|
+
canonical
|
|
909
|
+
}
|
|
910
|
+
OwnerTeamResolution::Unresolved { requested } => {
|
|
911
|
+
let outcome = refuse_owner_team_resolution(
|
|
912
|
+
store,
|
|
913
|
+
message_id,
|
|
914
|
+
event_log,
|
|
915
|
+
"owner_team_unresolved",
|
|
916
|
+
serde_json::json!({"owner_team_id": requested}),
|
|
917
|
+
DeliveryRefusal::UnknownRecipient,
|
|
918
|
+
)?;
|
|
919
|
+
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
920
|
+
}
|
|
921
|
+
OwnerTeamResolution::Ambiguous { requested, matches } => {
|
|
922
|
+
let outcome = refuse_owner_team_resolution(
|
|
923
|
+
store,
|
|
924
|
+
message_id,
|
|
925
|
+
event_log,
|
|
926
|
+
"owner_team_ambiguous",
|
|
927
|
+
serde_json::json!({"owner_team_id": requested, "matches": matches}),
|
|
928
|
+
DeliveryRefusal::Ambiguous,
|
|
929
|
+
)?;
|
|
930
|
+
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
if top_level_state_matches_owner_team(fallback, &canonical_team) {
|
|
934
|
+
let mut state = fallback.clone();
|
|
935
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
936
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
937
|
+
state,
|
|
938
|
+
canonical_team,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if top_level_state_matches_owner_team(&raw, &canonical_team) {
|
|
942
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
943
|
+
state: raw,
|
|
944
|
+
canonical_team,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
if state_has_no_team_entries(projection_source) {
|
|
948
|
+
let mut state = projection_source.clone();
|
|
949
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
950
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
951
|
+
state,
|
|
952
|
+
canonical_team,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
let mut state = project_state_for_owner_team_value(projection_source, &canonical_team)
|
|
956
|
+
.ok_or_else(|| MessagingError::Routing(format!("owner_team_unresolved: {canonical_team}")))?;
|
|
957
|
+
carry_top_level_leader_binding(&mut state, projection_source);
|
|
958
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
959
|
+
Ok(OwnerTeamProjection::Projected { state, canonical_team })
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
fn carry_top_level_leader_binding(projected: &mut serde_json::Value, raw: &serde_json::Value) {
|
|
963
|
+
let Some(projected_obj) = projected.as_object_mut() else {
|
|
964
|
+
return;
|
|
965
|
+
};
|
|
966
|
+
for key in ["leader_receiver", "team_owner", "owner_epoch"] {
|
|
967
|
+
if projected_obj.contains_key(key) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if let Some(value) = raw.get(key) {
|
|
971
|
+
projected_obj.insert(key.to_string(), value.clone());
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
fn state_has_no_team_entries(state: &serde_json::Value) -> bool {
|
|
977
|
+
state
|
|
978
|
+
.get("teams")
|
|
979
|
+
.and_then(serde_json::Value::as_object)
|
|
980
|
+
.is_none_or(serde_json::Map::is_empty)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
pub(crate) fn normalize_owner_team_id_rows(
|
|
984
|
+
workspace: &Path,
|
|
985
|
+
requested: &str,
|
|
986
|
+
canonical: &str,
|
|
987
|
+
message_id: Option<&str>,
|
|
988
|
+
event_log: Option<&EventLog>,
|
|
989
|
+
) -> Result<(), MessagingError> {
|
|
990
|
+
if requested == canonical {
|
|
991
|
+
return Ok(());
|
|
992
|
+
}
|
|
993
|
+
let store = MessageStore::open(workspace)?;
|
|
994
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
995
|
+
for table in [
|
|
996
|
+
"messages",
|
|
997
|
+
"results",
|
|
998
|
+
"scheduled_events",
|
|
999
|
+
"agent_health",
|
|
1000
|
+
"result_watchers",
|
|
1001
|
+
"leader_notification_log",
|
|
1002
|
+
] {
|
|
1003
|
+
let sql = format!("update or ignore {table} set owner_team_id = ?1 where owner_team_id = ?2");
|
|
1004
|
+
conn.execute(&sql, params![canonical, requested])?;
|
|
1005
|
+
}
|
|
1006
|
+
if let Some(event_log) = event_log {
|
|
1007
|
+
event_log.write(
|
|
1008
|
+
"owner_team_id.compatibility_alias_migrated",
|
|
1009
|
+
serde_json::json!({
|
|
1010
|
+
"requested_owner_team_id": requested,
|
|
1011
|
+
"canonical_owner_team_id": canonical,
|
|
1012
|
+
"message_id": message_id,
|
|
1013
|
+
}),
|
|
1014
|
+
)?;
|
|
1015
|
+
}
|
|
1016
|
+
Ok(())
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
fn refuse_owner_team_resolution(
|
|
1020
|
+
store: Option<&MessageStore>,
|
|
1021
|
+
message_id: Option<&str>,
|
|
1022
|
+
event_log: Option<&EventLog>,
|
|
1023
|
+
error: &str,
|
|
1024
|
+
details: serde_json::Value,
|
|
1025
|
+
refusal: DeliveryRefusal,
|
|
1026
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
1027
|
+
if let (Some(store), Some(message_id)) = (store, message_id) {
|
|
1028
|
+
store.mark(message_id, "failed", Some(error))?;
|
|
1029
|
+
}
|
|
1030
|
+
if let Some(event_log) = event_log {
|
|
1031
|
+
event_log.write(
|
|
1032
|
+
"owner_team_id.resolution_failed",
|
|
1033
|
+
serde_json::json!({
|
|
1034
|
+
"message_id": message_id,
|
|
1035
|
+
"error": error,
|
|
1036
|
+
"details": details,
|
|
1037
|
+
}),
|
|
1038
|
+
)?;
|
|
1039
|
+
}
|
|
1040
|
+
Ok(DeliveryOutcome {
|
|
1041
|
+
ok: false,
|
|
1042
|
+
status: DeliveryStatus::Refused,
|
|
1043
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
1044
|
+
message_id: message_id.map(str::to_string),
|
|
1045
|
+
verification: Some(error.to_string()),
|
|
1046
|
+
stage: None,
|
|
1047
|
+
reason: Some(refusal),
|
|
1048
|
+
channel: Some("owner_team_resolution".to_string()),
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
fn project_state_for_owner_team_value(
|
|
1053
|
+
raw: &serde_json::Value,
|
|
1054
|
+
team: &str,
|
|
1055
|
+
) -> Option<serde_json::Value> {
|
|
1056
|
+
if let Some(projected) = raw
|
|
1057
|
+
.get("teams")
|
|
1058
|
+
.and_then(serde_json::Value::as_object)
|
|
1059
|
+
.is_some_and(|teams| teams.contains_key(team))
|
|
1060
|
+
.then(|| crate::state::projection::project_top_level_view(raw, team))
|
|
1061
|
+
{
|
|
1062
|
+
return Some(projected);
|
|
1063
|
+
}
|
|
1064
|
+
if top_level_state_matches_owner_team(raw, team) {
|
|
1065
|
+
return None;
|
|
1066
|
+
}
|
|
1067
|
+
None
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
1071
|
+
state
|
|
1072
|
+
.get("active_team_key")
|
|
1073
|
+
.and_then(serde_json::Value::as_str)
|
|
1074
|
+
.is_some_and(|value| value == team)
|
|
1075
|
+
|| crate::state::projection::team_state_key(state) == team
|
|
1076
|
+
|| state
|
|
1077
|
+
.get("session_name")
|
|
1078
|
+
.and_then(serde_json::Value::as_str)
|
|
1079
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
1080
|
+
}
|
|
1081
|
+
|
|
721
1082
|
/// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答
|
|
722
1083
|
/// 后重注入 (查 pane_width fail-safe + attempt_trust_auto_answer + 等 dismissal + 重 inject)。
|
|
723
1084
|
pub fn retry_injection_after_trust_auto_answer(
|
|
@@ -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};
|