@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.
@@ -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::{InjectPayload, Key, PaneId, SessionName, Target, Transport, WindowName};
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
- if let Err(error) = transport.inject(
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
- if message.recipient == "leader" {
235
- store.mark(message_id, "failed", Some("leader_not_attached"))?;
236
- event_log.write(
237
- "leader_receiver.delivery_blocked",
238
- serde_json::json!({
239
- "message_id": message_id,
240
- "sender": message.sender,
241
- "reason": "leader_not_attached",
242
- "channel": "rebind_required",
243
- "action": "run team-agent claim-leader or team-agent takeover",
244
- "error": error.to_string(),
245
- }),
246
- )?;
247
- return Ok(DeliveryOutcome {
248
- ok: false,
249
- status: DeliveryStatus::Refused,
250
- message_status: MessageStatusShadow("failed".to_string()),
251
- message_id: Some(message_id.to_string()),
252
- verification: Some(
253
- "run team-agent claim-leader or team-agent takeover".to_string(),
254
- ),
255
- stage: None,
256
- reason: Some(DeliveryRefusal::LeaderNotAttached),
257
- channel: Some("rebind_required".to_string()),
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
- return Err(error.into());
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
- stamp_first_send_at_if_leader_to_worker(workspace, state, &message.sender, &message.recipient)?;
278
- record_turn_open_if_leader_to_worker(
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 = crate::state::persist::load_runtime_state(workspace)?;
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
- crate::state::persist::save_runtime_state(workspace, &state)?;
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 = crate::state::persist::load_runtime_state(workspace)?;
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
- crate::state::persist::save_runtime_state(workspace, &state)?;
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};